Skip to main content

tempo_transaction_pool/
tt_2d_pool.rs

1/// Basic 2D nonce pool for user nonces (nonce_key > 0) that are tracked on chain.
2use crate::{
3    metrics::AA2dPoolMetrics, ordering::TempoTipOrdering, transaction::TempoPooledTransaction,
4};
5use alloy_consensus::Transaction;
6use alloy_primitives::{
7    Address, B256, TxHash, U256,
8    map::{AddressMap, B256Map, HashMap, HashSet, U256Map, hash_map},
9};
10use reth_primitives_traits::transaction::error::InvalidTransactionError;
11use reth_tracing::tracing::trace;
12use reth_transaction_pool::{
13    AllPoolTransactions, BestTransactions, GetPooledTransactionLimit, PoolResult, PoolTransaction,
14    PriceBumpConfig, Priority, SubPool, SubPoolLimit, TransactionOrdering, TransactionOrigin,
15    ValidPoolTransaction,
16    error::{InvalidPoolTransactionError, PoolError, PoolErrorKind},
17    pool::{AddedPendingTransaction, AddedTransaction, QueuedReason, pending::PendingTransaction},
18};
19use revm::database::BundleAccount;
20use std::{
21    borrow::Borrow,
22    collections::{
23        BTreeMap, BTreeSet,
24        Bound::{Excluded, Unbounded},
25        btree_map::Entry,
26    },
27    sync::{
28        Arc,
29        atomic::{AtomicBool, Ordering},
30    },
31};
32use tempo_precompiles::NONCE_PRECOMPILE_ADDRESS;
33use tokio::sync::broadcast;
34
35type TxOrdering = TempoTipOrdering<TempoPooledTransaction>;
36type PoolUpdateResult = (
37    Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
38    Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
39);
40/// A sub-pool that keeps track of 2D nonce transactions.
41///
42/// It maintains both pending and queued transactions.
43///
44/// A 2d nonce transaction is pending if it dosn't have a nonce gap for its nonce key, and is queued if its nonce key set has nonce gaps.
45///
46/// This pool relies on state changes to track the nonces.
47///
48/// # Limitations
49///
50/// * We assume new AA transactions either create a new nonce key (nonce 0) or use an existing nonce key. To keep track of the known keys by accounts this pool relies on state changes to promote transactions to pending.
51#[derive(Debug)]
52pub struct AA2dPool {
53    /// Keeps track of transactions inserted in the pool.
54    ///
55    /// This way we can determine when transactions were submitted to the pool.
56    submission_id: u64,
57    /// Regular 2D nonce independent, pending, executable transactions, one per
58    /// sequence id.
59    ///
60    /// Expiring nonce transactions are not included here because they are keyed
61    /// by expiring nonce hash, not `AASequenceId`.
62    independent_transactions: HashMap<AASequenceId, AA2dStoredTransaction>,
63    /// _All_ regular 2D nonce transactions that are currently inside the pool,
64    /// grouped by their unique identifier.
65    ///
66    /// Expiring nonce transactions are not stored in `by_id`; they are tracked
67    /// separately by `expiring_nonce_txs`.
68    by_id: BTreeMap<AA2dTransactionId, Arc<AA2dInternalTransaction>>,
69    /// _All_ transactions by hash.
70    by_hash: B256Map<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
71    /// Expiring nonce transactions, keyed by expiring nonce hash (always pending/independent).
72    /// These use expiring nonce replay protection instead of sequential nonces.
73    expiring_nonce_txs: B256Map<AA2dStoredTransaction>,
74    /// Expiring nonce transactions in eviction order.
75    ///
76    /// Regular 2D transactions use `by_eviction_order`, which is keyed by
77    /// `AA2dTransactionId`. Expiring nonce transactions are always pending but
78    /// are not stored in `by_id`, so they need a separate ordered index. Each
79    /// key carries the transaction and a priority snapshot. The first entry is
80    /// the expiring nonce transaction that should be evicted next:
81    /// lowest priority first, then newest submission first when priorities tie.
82    expiring_nonce_eviction_order: BTreeSet<ExpiringNonceEvictionKey>,
83    /// A mapping of `expiring_nonce_seen` slot to expiring nonce hash.
84    ///
85    /// Used to track inclusion of expiring nonce transactions.
86    slot_to_expiring_nonce_hash: U256Map<B256>,
87    /// Scratch buffer reused while processing nonce state updates.
88    state_update_nonce_changes: HashMap<AASequenceId, u64>,
89    /// Scratch buffer reused while processing included expiring nonce transactions.
90    state_update_included_expiring_nonce_hashes: Vec<B256>,
91    /// Reverse index for the storage slot of an account's nonce
92    ///
93    /// ```solidity
94    ///  mapping(address => mapping(uint256 => uint64)) public nonces
95    /// ```
96    ///
97    /// This identifies the account and nonce key based on the slot in the `NonceManager`.
98    slot_to_seq_id: U256Map<AASequenceId>,
99    /// Settings for this sub-pool.
100    config: AA2dPoolConfig,
101    /// Metrics for tracking pool statistics
102    metrics: AA2dPoolMetrics,
103    /// All transactions ordered by eviction priority (lowest priority first).
104    ///
105    /// Rebuilt when the pool base fee changes. At eviction time, we scan this
106    /// set checking `is_pending` to find queued or pending transactions. Keys
107    /// own a priority snapshot so repricing does not mutate canonical
108    /// transaction storage.
109    by_eviction_order: BTreeSet<EvictionKey>,
110    /// Base fee used for transaction insertion and eviction-order priorities.
111    base_fee: u64,
112    /// Tracks the number of transactions per sender for DoS protection.
113    ///
114    /// Bounded by pool size (max unique senders = pending_limit + queued_limit).
115    /// Entries are removed when count reaches 0 via `decrement_sender_count`.
116    txs_by_sender: AddressMap<usize>,
117    /// Number of pending transactions, including expiring nonce transactions.
118    pending_count: usize,
119    /// Number of queued regular 2D nonce transactions.
120    queued_count: usize,
121    /// Used to broadcast new pending transactions to active [`BestAA2dTransactions`] iterators.
122    new_transaction_notifier: broadcast::Sender<AA2dStoredTransaction>,
123}
124
125impl Default for AA2dPool {
126    fn default() -> Self {
127        Self::new(AA2dPoolConfig::default())
128    }
129}
130
131impl AA2dPool {
132    /// Creates a new instance with the givenconfig and nonce keys
133    pub fn new(config: AA2dPoolConfig) -> Self {
134        let (new_transaction_notifier, _) = broadcast::channel(200);
135        Self {
136            submission_id: 0,
137            independent_transactions: Default::default(),
138            by_id: Default::default(),
139            by_hash: Default::default(),
140            expiring_nonce_txs: Default::default(),
141            expiring_nonce_eviction_order: Default::default(),
142            slot_to_expiring_nonce_hash: Default::default(),
143            state_update_nonce_changes: Default::default(),
144            state_update_included_expiring_nonce_hashes: Default::default(),
145            slot_to_seq_id: Default::default(),
146            config,
147            metrics: AA2dPoolMetrics::default(),
148            by_eviction_order: Default::default(),
149            base_fee: 0,
150            txs_by_sender: Default::default(),
151            pending_count: 0,
152            queued_count: 0,
153            new_transaction_notifier,
154        }
155    }
156
157    /// Broadcasts a new pending transaction to all active [`BestAA2dTransactions`] iterators.
158    fn notify_new_pending(&self, tx: &AA2dStoredTransaction) {
159        if self.new_transaction_notifier.receiver_count() > 0 {
160            let _ = self.new_transaction_notifier.send(tx.clone());
161        }
162    }
163
164    /// Updates all metrics to reflect the current state of the pool
165    fn update_metrics(&self) {
166        let (pending, queued) = self.pending_and_queued_txn_count();
167        let total = self.by_id.len() + self.expiring_nonce_txs.len();
168        self.metrics.set_transaction_counts(total, pending, queued);
169    }
170
171    pub(crate) fn set_base_fee(&mut self, base_fee: u64) {
172        if self.base_fee == base_fee {
173            return;
174        }
175
176        self.base_fee = base_fee;
177        self.rebuild_eviction_order();
178    }
179
180    fn rebuild_eviction_order(&mut self) {
181        self.by_eviction_order.clear();
182        for (id, tx) in &self.by_id {
183            self.by_eviction_order.insert(EvictionKey::with_base_fee(
184                Arc::clone(tx),
185                *id,
186                self.base_fee,
187            ));
188        }
189
190        self.expiring_nonce_eviction_order.clear();
191        for tx in self.expiring_nonce_txs.values() {
192            self.expiring_nonce_eviction_order.insert(
193                ExpiringNonceEvictionKey::from_pending_with_base_fee(tx, self.base_fee),
194            );
195        }
196    }
197
198    /// Entrypoint for adding a 2d AA transaction.
199    ///
200    /// `on_chain_nonce` is expected to be the nonce of the sender at the time of validation.
201    /// If transaction is using 2D nonces, this is expected to be the nonce corresponding
202    /// to the transaction's nonce key.
203    ///
204    /// `hardfork` indicates the active Tempo hardfork. When T1 or later, expiring nonce
205    /// transactions (nonce_key == U256::MAX) are handled specially. Otherwise, they are
206    /// treated as regular 2D nonce transactions.
207    pub(crate) fn add_transaction(
208        &mut self,
209        transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
210        on_chain_nonce: u64,
211        hardfork: tempo_chainspec::hardfork::TempoHardfork,
212    ) -> PoolResult<AddedTransaction<TempoPooledTransaction>> {
213        debug_assert!(
214            transaction.transaction.is_aa(),
215            "only AA transactions are supported"
216        );
217        // Handle expiring nonce transactions separately - they use expiring nonce hash as unique ID
218        // Only treat as expiring nonce if T1 hardfork is active.
219        //
220        // No `by_hash` duplicate check needed here: a duplicate transaction maps to the same
221        // expiring nonce hash, which `add_expiring_nonce_transaction` rejects.
222        if hardfork.is_t1() && transaction.transaction.is_expiring_nonce() {
223            return self.add_expiring_nonce_transaction(transaction);
224        }
225
226        if self.contains(transaction.hash()) {
227            return Err(PoolError::new(
228                *transaction.hash(),
229                PoolErrorKind::AlreadyImported,
230            ));
231        }
232
233        let tx_id = transaction
234            .transaction
235            .aa_transaction_id()
236            .expect("Transaction added to AA2D pool must be an AA transaction");
237
238        if transaction.nonce() < on_chain_nonce {
239            // outdated transaction
240            return Err(PoolError::new(
241                *transaction.hash(),
242                PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus(
243                    InvalidTransactionError::NonceNotConsistent {
244                        tx: transaction.nonce(),
245                        state: on_chain_nonce,
246                    },
247                )),
248            ));
249        }
250
251        // assume the transaction is not pending, will get updated later
252        let tx = Arc::new(AA2dInternalTransaction {
253            inner: AA2dStoredTransaction::new(self.next_id(), transaction.clone()),
254            is_pending: AtomicBool::new(false),
255        });
256
257        // Use entry API once to both check for replacement and insert.
258        // This avoids a separate contains_key lookup.
259        let sender = transaction.sender();
260        let replaced = match self.by_id.entry(tx_id) {
261            Entry::Occupied(mut entry) => {
262                // Ensure the replacement transaction is not underpriced
263                if entry
264                    .get()
265                    .inner
266                    .transaction
267                    .is_underpriced(&tx.inner.transaction, &self.config.price_bump_config)
268                {
269                    return Err(PoolError::new(
270                        *transaction.hash(),
271                        PoolErrorKind::ReplacementUnderpriced,
272                    ));
273                }
274
275                let replaced = entry.insert(Arc::clone(&tx));
276                self.remove_from_counts(replaced.is_pending());
277                self.queued_count += 1;
278                Some(replaced)
279            }
280            Entry::Vacant(entry) => {
281                // Check per-sender limit and increment the count for new (non-replacement)
282                // transactions with a single map lookup
283                match self.txs_by_sender.entry(sender) {
284                    hash_map::Entry::Occupied(mut count) => {
285                        if *count.get() >= self.config.max_txs_per_sender {
286                            return Err(PoolError::new(
287                                *transaction.hash(),
288                                PoolErrorKind::SpammerExceededCapacity(sender),
289                            ));
290                        }
291                        *count.get_mut() += 1;
292                    }
293                    hash_map::Entry::Vacant(count) => {
294                        if self.config.max_txs_per_sender == 0 {
295                            return Err(PoolError::new(
296                                *transaction.hash(),
297                                PoolErrorKind::SpammerExceededCapacity(sender),
298                            ));
299                        }
300                        count.insert(1);
301                    }
302                }
303
304                entry.insert(Arc::clone(&tx));
305                self.queued_count += 1;
306                None
307            }
308        };
309
310        // Cache the nonce key slot for reverse lookup, if this transaction uses 2D nonce.
311        // This must happen after successful by_id insertion to avoid leaking slot entries
312        // when the transaction is rejected (e.g., by per-sender limit or replacement check).
313        if transaction.transaction.is_aa_2d() {
314            self.record_2d_slot(&transaction.transaction);
315        }
316
317        // clean up replaced
318        if let Some(replaced) = &replaced {
319            // we only need to remove it from the hash list, because we already replaced it in the by id set,
320            // and if this is the independent transaction, it will be replaced by the new transaction below
321            self.by_hash.remove(replaced.inner.transaction.hash());
322            // Remove from eviction set
323            self.remove_eviction_key(replaced);
324        }
325
326        // insert transaction by hash
327        self.by_hash
328            .insert(*tx.inner.transaction.hash(), tx.inner.transaction.clone());
329
330        // contains transactions directly impacted by the new transaction (filled nonce gap)
331        let mut promoted = Vec::new();
332        // Track whether this transaction was inserted as pending
333        let mut inserted_as_pending = false;
334        let mut newly_pending = 0usize;
335        // now we need to scan the range and mark transactions as pending, if any
336        let on_chain_id = AA2dTransactionId::new(tx_id.seq_id, on_chain_nonce);
337        // track the next nonce we expect if the transactions are gapless
338        let mut next_nonce = on_chain_id.nonce;
339
340        // scan all the transactions with the same nonce key starting with the on chain nonce
341        // to check if our new transaction was inserted as pending and perhaps promoted more transactions
342        for (existing_id, existing_tx) in self.descendant_txs(&on_chain_id) {
343            if existing_id.nonce == next_nonce {
344                match existing_id.nonce.cmp(&tx_id.nonce) {
345                    std::cmp::Ordering::Less => {
346                        // unaffected by our transaction
347                    }
348                    std::cmp::Ordering::Equal => {
349                        if !existing_tx.set_pending(true) {
350                            newly_pending += 1;
351                        }
352                        inserted_as_pending = true;
353                    }
354                    std::cmp::Ordering::Greater => {
355                        // if this was previously not pending we need to promote the transaction
356                        let was_pending = existing_tx.set_pending(true);
357                        if !was_pending {
358                            newly_pending += 1;
359                            promoted.push(existing_tx.inner.clone());
360                        } else {
361                            // already pending, so the rest of the contiguous sequence is
362                            // already pending as well
363                            break;
364                        }
365                    }
366                }
367                // continue ungapped sequence
368                next_nonce = existing_id.nonce.saturating_add(1);
369            } else {
370                // can exit early here because we hit a nonce gap
371                break;
372            }
373        }
374        self.pending_count += newly_pending;
375        self.queued_count -= newly_pending;
376
377        // Record metrics
378        self.metrics.inc_inserted();
379
380        // Create eviction key for the new transaction and add to the single eviction set
381        let new_tx_eviction_key = EvictionKey::with_base_fee(Arc::clone(&tx), tx_id, self.base_fee);
382        self.by_eviction_order.insert(new_tx_eviction_key);
383
384        if inserted_as_pending {
385            if !promoted.is_empty() {
386                self.metrics.inc_promoted(promoted.len());
387            }
388            // if this is the next nonce in line we can mark it as independent
389            if tx_id.nonce == on_chain_nonce {
390                self.independent_transactions
391                    .insert(tx_id.seq_id, tx.inner.clone());
392            }
393
394            // Notify active BestAA2dTransactions iterators about new pending transactions.
395            self.notify_new_pending(&tx.inner);
396            for promoted_tx in &promoted {
397                self.notify_new_pending(promoted_tx);
398            }
399
400            return Ok(AddedTransaction::Pending(AddedPendingTransaction {
401                transaction,
402                replaced: replaced.map(|tx| tx.inner.transaction.clone()),
403                promoted: promoted.into_iter().map(|tx| tx.transaction).collect(),
404                discarded: self.discard(),
405            }));
406        }
407
408        // Call discard for queued transactions too
409        let _ = self.discard();
410
411        Ok(AddedTransaction::Parked {
412            transaction,
413            replaced: replaced.map(|tx| tx.inner.transaction.clone()),
414            subpool: SubPool::Queued,
415            queued_reason: Some(QueuedReason::NonceGap),
416        })
417    }
418
419    /// Adds an expiring nonce transaction to the pool.
420    ///
421    /// Expiring nonce transactions use the expiring nonce hash as their unique identifier instead
422    /// of (sender, nonce_key, nonce). They are always immediately pending since they don't have
423    /// sequential nonce dependencies.
424    fn add_expiring_nonce_transaction(
425        &mut self,
426        transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
427    ) -> PoolResult<AddedTransaction<TempoPooledTransaction>> {
428        let tx_hash = *transaction.hash();
429        let expiring_nonce_hash = transaction.transaction.precomputed_expiring_nonce_hash();
430
431        let expiring_nonce_entry = match self.expiring_nonce_txs.entry(expiring_nonce_hash) {
432            hash_map::Entry::Occupied(_) => {
433                return Err(PoolError::new(tx_hash, PoolErrorKind::AlreadyImported));
434            }
435            hash_map::Entry::Vacant(entry) => entry,
436        };
437
438        // Check per-sender limit and increment the count with a single map lookup
439        let sender = transaction.sender();
440        match self.txs_by_sender.entry(sender) {
441            hash_map::Entry::Occupied(mut count) => {
442                if *count.get() >= self.config.max_txs_per_sender {
443                    return Err(PoolError::new(
444                        tx_hash,
445                        PoolErrorKind::SpammerExceededCapacity(sender),
446                    ));
447                }
448                *count.get_mut() += 1;
449            }
450            hash_map::Entry::Vacant(count) => {
451                if self.config.max_txs_per_sender == 0 {
452                    return Err(PoolError::new(
453                        tx_hash,
454                        PoolErrorKind::SpammerExceededCapacity(sender),
455                    ));
456                }
457                count.insert(1);
458            }
459        }
460
461        // Create pending transaction
462        let pending_tx = AA2dStoredTransaction {
463            submission_id: {
464                let id = self.submission_id;
465                self.submission_id = self.submission_id.wrapping_add(1);
466                id
467            },
468            transaction: transaction.clone(),
469        };
470        let eviction_key =
471            ExpiringNonceEvictionKey::from_pending_with_base_fee(&pending_tx, self.base_fee);
472        let pending_tx_update = if self.new_transaction_notifier.receiver_count() > 0 {
473            Some(pending_tx.clone())
474        } else {
475            None
476        };
477
478        // Insert into expiring nonce map and by_hash
479        expiring_nonce_entry.insert(pending_tx);
480        self.expiring_nonce_eviction_order.insert(eviction_key);
481        if let Some(slot) = transaction.transaction.expiring_nonce_slot() {
482            self.slot_to_expiring_nonce_hash
483                .insert(slot, expiring_nonce_hash);
484        }
485        self.by_hash.insert(tx_hash, transaction.clone());
486
487        self.pending_count += 1;
488
489        trace!(target: "txpool", hash = %tx_hash, "Added expiring nonce transaction");
490
491        self.update_metrics();
492
493        // Notify active BestAA2dTransactions iterators about the new pending transaction
494        if let Some(pending_tx) = pending_tx_update {
495            let _ = self.new_transaction_notifier.send(pending_tx);
496        }
497
498        // Expiring nonce transactions are always immediately pending
499        Ok(AddedTransaction::Pending(AddedPendingTransaction {
500            transaction,
501            replaced: None,
502            promoted: vec![],
503            discarded: self.discard(),
504        }))
505    }
506
507    /// Returns how many pending and queued transactions are in the pool.
508    pub(crate) fn pending_and_queued_txn_count(&self) -> (usize, usize) {
509        (self.pending_count, self.queued_count)
510    }
511
512    /// Scans the pool to recompute counts for invariant checks.
513    #[cfg(test)]
514    fn scan_pending_and_queued_txn_count(&self) -> (usize, usize) {
515        let (pending_2d, queued_2d) = self.by_id.values().fold((0, 0), |mut acc, tx| {
516            if tx.is_pending() {
517                acc.0 += 1;
518            } else {
519                acc.1 += 1;
520            }
521            acc
522        });
523        // Expiring nonce txs are always pending
524        let expiring_pending = self.expiring_nonce_txs.len();
525        (pending_2d + expiring_pending, queued_2d)
526    }
527
528    /// Returns all transactions that where submitted with the given [`TransactionOrigin`]
529    pub(crate) fn get_transactions_by_origin_iter(
530        &self,
531        origin: TransactionOrigin,
532    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> + '_ {
533        let regular = self
534            .by_id
535            .values()
536            .filter(move |tx| tx.inner.transaction.origin == origin)
537            .map(|tx| tx.inner.transaction.clone());
538        let expiring = self
539            .expiring_nonce_txs
540            .values()
541            .filter(move |tx| tx.transaction.origin == origin)
542            .map(|tx| tx.transaction.clone());
543        regular.chain(expiring)
544    }
545
546    /// Returns all transactions that where submitted with the given [`TransactionOrigin`]
547    pub(crate) fn get_pending_transactions_by_origin_iter(
548        &self,
549        origin: TransactionOrigin,
550    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> + '_ {
551        let regular = self
552            .by_id
553            .values()
554            .filter(move |tx| tx.is_pending() && tx.inner.transaction.origin == origin)
555            .map(|tx| tx.inner.transaction.clone());
556        // Expiring nonce txs are always pending
557        let expiring = self
558            .expiring_nonce_txs
559            .values()
560            .filter(move |tx| tx.transaction.origin == origin)
561            .map(|tx| tx.transaction.clone());
562        regular.chain(expiring)
563    }
564
565    /// Returns all transactions of the address
566    pub(crate) fn get_transactions_by_sender_iter(
567        &self,
568        sender: Address,
569    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> + '_ {
570        let regular = self
571            .by_id
572            .values()
573            .filter(move |tx| tx.inner.transaction.sender() == sender)
574            .map(|tx| tx.inner.transaction.clone());
575        let expiring = self
576            .expiring_nonce_txs
577            .values()
578            .filter(move |tx| tx.transaction.sender() == sender)
579            .map(|tx| tx.transaction.clone());
580        regular.chain(expiring)
581    }
582
583    /// Returns an iterator over all transaction hashes in this pool
584    pub(crate) fn all_transaction_hashes_iter(&self) -> impl Iterator<Item = TxHash> {
585        self.by_hash.keys().copied()
586    }
587
588    /// Returns all transactions from that are queued.
589    pub(crate) fn queued_transactions(
590        &self,
591    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
592        self.by_id
593            .values()
594            .filter(|tx| !tx.is_pending())
595            .map(|tx| tx.inner.transaction.clone())
596    }
597
598    /// Returns all transactions that are pending.
599    pub(crate) fn pending_transactions(
600        &self,
601    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> + '_ {
602        // Include both regular pending 2D nonce txs and expiring nonce txs
603        let regular_pending = self
604            .by_id
605            .values()
606            .filter(|tx| tx.is_pending())
607            .map(|tx| tx.inner.transaction.clone());
608        let expiring_pending = self
609            .expiring_nonce_txs
610            .values()
611            .map(|tx| tx.transaction.clone());
612        regular_pending.chain(expiring_pending)
613    }
614
615    /// Appends all transactions to the provided collection.
616    pub(crate) fn append_all_transactions(
617        &self,
618        transactions: &mut AllPoolTransactions<TempoPooledTransaction>,
619    ) {
620        transactions.pending.reserve(self.pending_count);
621        transactions.queued.reserve(self.queued_count);
622
623        for tx in self.by_id.values() {
624            if tx.is_pending() {
625                transactions.pending.push(tx.inner.transaction.clone());
626            } else {
627                transactions.queued.push(tx.inner.transaction.clone());
628            }
629        }
630
631        transactions.pending.extend(
632            self.expiring_nonce_txs
633                .values()
634                .map(|tx| tx.transaction.clone()),
635        );
636    }
637
638    /// Returns the best, executable transactions for this sub-pool
639    pub(crate) fn best_transactions(&self) -> BestAA2dTransactions {
640        self.best_transactions_with_base_fee(self.base_fee)
641    }
642
643    /// Returns the best, executable transactions for this sub-pool at `base_fee`.
644    #[expect(clippy::mutable_key_type)]
645    pub(crate) fn best_transactions_with_base_fee(&self, base_fee: u64) -> BestAA2dTransactions {
646        let expiring_nonce_order = if base_fee == self.base_fee {
647            self.expiring_nonce_eviction_order.clone()
648        } else {
649            self.expiring_nonce_txs
650                .values()
651                .map(|tx| ExpiringNonceEvictionKey::from_pending_with_base_fee(tx, base_fee))
652                .collect()
653        };
654        let independent = self
655            .independent_transactions
656            .values()
657            .filter_map(|tx| {
658                let id = tx
659                    .transaction
660                    .transaction
661                    .aa_transaction_id()
662                    .expect("Independent transaction must have AA transaction ID");
663                let tx = self.by_id.get(&id)?;
664                Some(tx.inner.clone_into_pending(base_fee))
665            })
666            .collect();
667
668        BestAA2dTransactions {
669            independent,
670            by_id: self
671                .by_id
672                .iter()
673                .filter(|(_, tx)| tx.is_pending())
674                .map(|(id, tx)| (*id, tx.inner.clone()))
675                .collect(),
676            expiring_nonce_order,
677            invalid: Default::default(),
678            new_transaction_receiver: Some(self.new_transaction_notifier.subscribe()),
679            last_priority: None,
680            base_fee,
681        }
682    }
683
684    /// Returns the transaction by hash.
685    pub(crate) fn get(
686        &self,
687        tx_hash: &TxHash,
688    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
689        self.by_hash.get(tx_hash).cloned()
690    }
691
692    /// Returns the transaction by hash.
693    pub(crate) fn get_all<'a, I>(
694        &self,
695        tx_hashes: I,
696    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
697    where
698        I: Iterator<Item = &'a TxHash> + 'a,
699    {
700        let mut ret = Vec::new();
701        for tx_hash in tx_hashes {
702            if let Some(tx) = self.get(tx_hash) {
703                ret.push(tx);
704            }
705        }
706        ret
707    }
708
709    /// Returns pooled transaction elements for the given hashes while respecting the size limit.
710    ///
711    /// This method collects transactions from the pool, converts them to pooled format,
712    /// and tracks the accumulated size. It stops collecting when the limit is exceeded.
713    ///
714    /// The `accumulated_size` is updated with the total encoded size of returned transactions.
715    pub(crate) fn append_pooled_transaction_elements<'a>(
716        &self,
717        tx_hashes: impl IntoIterator<Item = &'a TxHash>,
718        limit: GetPooledTransactionLimit,
719        accumulated_size: &mut usize,
720        out: &mut Vec<<TempoPooledTransaction as PoolTransaction>::Pooled>,
721    ) {
722        for tx_hash in tx_hashes {
723            let Some(tx) = self.by_hash.get(tx_hash) else {
724                continue;
725            };
726
727            let encoded_len = tx.transaction.encoded_length();
728            let Some(pooled) = tx.transaction.clone_into_pooled().ok() else {
729                continue;
730            };
731
732            *accumulated_size += encoded_len;
733            out.push(pooled.into_inner());
734
735            if limit.exceeds(*accumulated_size) {
736                break;
737            }
738        }
739    }
740
741    /// Returns an iterator over all senders in this pool.
742    pub(crate) fn senders_iter(&self) -> impl Iterator<Item = &Address> {
743        let regular = self
744            .by_id
745            .values()
746            .map(|tx| tx.inner.transaction.sender_ref());
747        let expiring = self
748            .expiring_nonce_txs
749            .values()
750            .map(|tx| tx.transaction.sender_ref());
751        regular.chain(expiring)
752    }
753
754    /// Returns all transactions that _follow_ after the given id but have the same sender.
755    ///
756    /// NOTE: The range is _inclusive_: if the transaction that belongs to `id` it will be the
757    /// first value.
758    fn descendant_txs<'a, 'b: 'a>(
759        &'a self,
760        id: &'b AA2dTransactionId,
761    ) -> impl Iterator<Item = (&'a AA2dTransactionId, &'a Arc<AA2dInternalTransaction>)> + 'a {
762        self.by_id
763            .range(id..)
764            .take_while(|(other, _)| id.seq_id == other.seq_id)
765    }
766
767    /// Returns all transactions that _follow_ after the given id and have the same sender.
768    ///
769    /// NOTE: The range is _exclusive_
770    fn descendant_txs_exclusive<'a, 'b: 'a>(
771        &'a self,
772        id: &'b AA2dTransactionId,
773    ) -> impl Iterator<Item = (&'a AA2dTransactionId, &'a Arc<AA2dInternalTransaction>)> + 'a {
774        self.by_id
775            .range((Excluded(id), Unbounded))
776            .take_while(|(other, _)| id.seq_id == other.seq_id)
777    }
778
779    /// Removes the transaction with the given id from all sets.
780    ///
781    /// This does __not__ shift the independent transaction forward or mark descendants as pending.
782    fn remove_transaction_by_id(
783        &mut self,
784        id: &AA2dTransactionId,
785    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
786        let tx = self.by_id.remove(id)?;
787
788        // Remove from eviction set
789        self.remove_eviction_key(&tx);
790
791        // Clean up cached nonce key slots if this was the last transaction of the sequence
792        if self.by_id.range(id.seq_id.range()).next().is_none()
793            && let Some(slot) = tx.inner.transaction.transaction.nonce_key_slot()
794        {
795            self.slot_to_seq_id.remove(&slot);
796        }
797
798        self.remove_independent(id);
799        let removed_tx = tx.inner.transaction.clone();
800        self.by_hash.remove(removed_tx.hash());
801        self.remove_from_counts(tx.is_pending());
802
803        // Decrement sender count
804        self.decrement_sender_count(removed_tx.sender());
805
806        Some(removed_tx)
807    }
808
809    /// Decrements the transaction count for a sender, removing the entry if it reaches zero.
810    fn decrement_sender_count(&mut self, sender: Address) {
811        if let hash_map::Entry::Occupied(mut entry) = self.txs_by_sender.entry(sender) {
812            let count = entry.get_mut();
813            *count -= 1;
814            if *count == 0 {
815                entry.remove();
816            }
817        }
818    }
819
820    fn remove_eviction_key(&mut self, tx: &Arc<AA2dInternalTransaction>) {
821        self.by_eviction_order.remove(&EvictionOrderKey::new(
822            TempoTipOrdering::default().priority(&tx.inner.transaction.transaction, self.base_fee),
823            tx.inner.submission_id,
824        ));
825    }
826
827    /// Removes the independent transaction if it matches the given id.
828    fn remove_independent(&mut self, id: &AA2dTransactionId) -> Option<AA2dStoredTransaction> {
829        // Only remove from independent_transactions if this is the independent transaction
830        match self.independent_transactions.entry(id.seq_id) {
831            hash_map::Entry::Occupied(entry) => {
832                // we know it's the independent tx if the tracked tx has the same nonce
833                if entry.get().transaction.nonce() == id.nonce {
834                    return Some(entry.remove());
835                }
836            }
837            hash_map::Entry::Vacant(_) => {}
838        };
839        None
840    }
841
842    /// Removes the transaction by its hash from all internal sets.
843    ///
844    /// This batches demotion by seq_id to avoid O(N*N) complexity when removing many
845    /// transactions from the same sequence.
846    pub(crate) fn remove_transactions<'a, I>(
847        &mut self,
848        tx_hashes: I,
849    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
850    where
851        I: Iterator<Item = &'a TxHash> + 'a,
852    {
853        let mut txs = Vec::new();
854        let mut seq_ids_to_demote: HashMap<AASequenceId, u64> = HashMap::default();
855
856        for tx_hash in tx_hashes {
857            if let Some((tx, seq_id)) = self.remove_transaction_by_hash_no_demote(tx_hash) {
858                if let Some(id) = seq_id {
859                    seq_ids_to_demote
860                        .entry(id.seq_id)
861                        .and_modify(|min_nonce| {
862                            if id.nonce < *min_nonce {
863                                *min_nonce = id.nonce;
864                            }
865                        })
866                        .or_insert(id.nonce);
867                }
868                txs.push(tx);
869            }
870        }
871
872        // Demote once per seq_id, starting from the minimum removed nonce
873        for (seq_id, min_nonce) in seq_ids_to_demote {
874            self.demote_from_nonce(&seq_id, min_nonce);
875        }
876
877        txs
878    }
879
880    /// Removes the transaction by its hash from all internal sets.
881    ///
882    /// This does __not__ shift the independent transaction forward but it does demote descendants
883    /// to queued status since removing a transaction creates a nonce gap.
884    fn remove_transaction_by_hash(
885        &mut self,
886        tx_hash: &B256,
887    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
888        let (tx, id) = self.remove_transaction_by_hash_no_demote(tx_hash)?;
889
890        // Demote all descendants to queued status since removing this transaction creates a gap
891        if let Some(id) = id {
892            self.demote_descendants(&id);
893        }
894
895        Some(tx)
896    }
897
898    /// Internal helper that removes a transaction without demoting descendants.
899    ///
900    /// Returns the removed transaction and its AA2dTransactionId (if it was a 2D nonce tx).
901    fn remove_transaction_by_hash_no_demote(
902        &mut self,
903        tx_hash: &B256,
904    ) -> Option<(
905        Arc<ValidPoolTransaction<TempoPooledTransaction>>,
906        Option<AA2dTransactionId>,
907    )> {
908        let tx = self.by_hash.remove(tx_hash)?;
909
910        // Check if this is an expiring nonce transaction
911        if tx.transaction.is_expiring_nonce() {
912            let tx =
913                self.remove_expiring_nonce_tx(&tx.transaction.precomputed_expiring_nonce_hash())?;
914            return Some((tx, None));
915        }
916
917        // Regular 2D nonce transaction
918        let id = tx
919            .transaction
920            .aa_transaction_id()
921            .expect("is AA transaction");
922        self.remove_transaction_by_id(&id)?;
923
924        Some((tx, Some(id)))
925    }
926
927    /// Demotes all descendants of the given transaction to queued status (`is_pending = false`).
928    ///
929    /// This should be called after removing a transaction to ensure descendants don't remain
930    /// marked as pending when they're no longer executable due to the nonce gap.
931    fn demote_descendants(&mut self, id: &AA2dTransactionId) {
932        self.demote_from_nonce(&id.seq_id, id.nonce);
933    }
934
935    /// Demotes all transactions for a seq_id with nonce > min_nonce to queued status.
936    ///
937    /// This is used both for single-tx removal (demote_descendants) and batch removal
938    /// where we want to demote once per seq_id starting from the minimum removed nonce.
939    fn demote_from_nonce(&mut self, seq_id: &AASequenceId, min_nonce: u64) {
940        let start_id = AA2dTransactionId::new(*seq_id, min_nonce);
941        for (_, tx) in self
942            .by_id
943            .range((Excluded(&start_id), Unbounded))
944            .take_while(|(other, _)| *seq_id == other.seq_id)
945        {
946            if tx.set_pending(false) {
947                self.pending_count -= 1;
948                self.queued_count += 1;
949            }
950        }
951    }
952
953    /// Removes and returns all matching transactions and their dependent transactions from the
954    /// pool.
955    pub(crate) fn remove_transactions_and_descendants<'a, I>(
956        &mut self,
957        hashes: I,
958    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
959    where
960        I: Iterator<Item = &'a TxHash> + 'a,
961    {
962        let mut removed = Vec::new();
963        for hash in hashes {
964            if let Some(tx) = self.remove_transaction_by_hash(hash) {
965                let id = tx.transaction.aa_transaction_id();
966                removed.push(tx);
967                if let Some(id) = id {
968                    self.remove_descendants(&id, &mut removed);
969                }
970            }
971        }
972        removed
973    }
974
975    /// Removes all transactions from the given sender.
976    pub(crate) fn remove_transactions_by_sender(
977        &mut self,
978        sender_id: Address,
979    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
980        let mut removed = Vec::new();
981        let txs = self
982            .get_transactions_by_sender_iter(sender_id)
983            .collect::<Vec<_>>();
984        for tx in txs {
985            if tx.transaction.is_expiring_nonce() {
986                if let Some(tx) =
987                    self.remove_expiring_nonce_tx(&tx.transaction.precomputed_expiring_nonce_hash())
988                {
989                    removed.push(tx);
990                }
991            } else if let Some(tx) = tx
992                .transaction
993                .aa_transaction_id()
994                .and_then(|id| self.remove_transaction_by_id(&id))
995            {
996                removed.push(tx);
997            }
998        }
999        removed
1000    }
1001
1002    /// Removes _only_ the descendants of the given transaction from this pool.
1003    ///
1004    /// All removed transactions are added to the `removed` vec.
1005    fn remove_descendants(
1006        &mut self,
1007        tx: &AA2dTransactionId,
1008        removed: &mut Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
1009    ) {
1010        let mut id = *tx;
1011
1012        // this will essentially pop _all_ descendant transactions one by one
1013        loop {
1014            let descendant = self.descendant_txs_exclusive(&id).map(|(id, _)| *id).next();
1015            if let Some(descendant) = descendant {
1016                if let Some(tx) = self.remove_transaction_by_id(&descendant) {
1017                    removed.push(tx)
1018                }
1019                id = descendant;
1020            } else {
1021                return;
1022            }
1023        }
1024    }
1025
1026    /// Updates the internal state based on the state changes of the `NonceManager` [`NONCE_PRECOMPILE_ADDRESS`].
1027    ///
1028    /// This takes changed [`AASequenceId`]s with their current on-chain nonce.
1029    ///
1030    /// This will prune mined transactions and promote unblocked transactions if any, returns `(promoted, mined)`
1031    #[cfg(test)]
1032    pub(crate) fn on_nonce_changes(
1033        &mut self,
1034        on_chain_ids: HashMap<AASequenceId, u64>,
1035    ) -> PoolUpdateResult {
1036        trace!(target: "txpool::2d", ?on_chain_ids, "processing nonce changes");
1037
1038        self.on_nonce_changes_iter(on_chain_ids)
1039    }
1040
1041    fn on_nonce_changes_iter(
1042        &mut self,
1043        on_chain_ids: impl IntoIterator<Item = (AASequenceId, u64)>,
1044    ) -> PoolUpdateResult {
1045        let mut promoted = Vec::new();
1046        let mut mined_ids = Vec::new();
1047
1048        // we assume the set of changed senders is smaller than the individual accounts
1049        'changes: for (sender_id, on_chain_nonce) in on_chain_ids {
1050            let mut iter = self
1051                .by_id
1052                .range_mut((sender_id.start_bound(), Unbounded))
1053                .take_while(move |(other, _)| sender_id == other.seq_id)
1054                .peekable();
1055
1056            let Some(mut current) = iter.next() else {
1057                continue;
1058            };
1059
1060            // track mined transactions
1061            'mined: loop {
1062                if current.0.nonce < on_chain_nonce {
1063                    mined_ids.push(*current.0);
1064                    let Some(next) = iter.next() else {
1065                        continue 'changes;
1066                    };
1067                    current = next;
1068                } else {
1069                    break 'mined;
1070                }
1071            }
1072
1073            // Process remaining transactions starting from `current` (which is >= on_chain_nonce)
1074            let mut next_nonce = on_chain_nonce;
1075            let mut newly_pending = 0usize;
1076            let mut newly_queued = 0usize;
1077            for (existing_id, existing_tx) in std::iter::once(current).chain(iter) {
1078                if existing_id.nonce == next_nonce {
1079                    // Promote if transaction was previously queued (not pending)
1080                    let was_pending = existing_tx.set_pending(true);
1081                    if !was_pending {
1082                        newly_pending += 1;
1083                        promoted.push(existing_tx.inner.transaction.clone());
1084                    }
1085
1086                    if existing_id.nonce == on_chain_nonce {
1087                        // if this is the on chain nonce we can mark it as the next independent transaction
1088                        self.independent_transactions
1089                            .insert(existing_id.seq_id, existing_tx.inner.clone());
1090                    }
1091
1092                    next_nonce = next_nonce.saturating_add(1);
1093                } else {
1094                    // Gap detected - mark this and all remaining transactions as non-pending
1095                    if existing_tx.set_pending(false) {
1096                        newly_queued += 1;
1097                    }
1098                }
1099            }
1100            self.pending_count += newly_pending;
1101            self.queued_count -= newly_pending;
1102            self.pending_count -= newly_queued;
1103            self.queued_count += newly_queued;
1104
1105            // If no transaction was found at the on-chain nonce (next_nonce unchanged),
1106            // remove any stale independent transaction entry for this seq_id.
1107            // This handles reorgs where the on-chain nonce decreases.
1108            if next_nonce == on_chain_nonce {
1109                self.independent_transactions.remove(&sender_id);
1110            }
1111        }
1112
1113        // actually remove mined transactions
1114        let mut mined = Vec::with_capacity(mined_ids.len());
1115        for id in mined_ids {
1116            if let Some(removed) = self.remove_transaction_by_id(&id) {
1117                mined.push(removed);
1118            }
1119        }
1120
1121        (promoted, mined)
1122    }
1123
1124    /// Removes lowest-priority transactions if the pool is above capacity.
1125    ///
1126    /// This evicts transactions with the lowest priority (based on [`TempoTipOrdering`])
1127    /// to prevent DoS attacks where adversaries use vanity addresses with many leading zeroes
1128    /// to avoid eviction.
1129    ///
1130    /// Evicts queued transactions first (up to queued_limit), then pending if needed.
1131    /// Counts are computed lazily by scanning the eviction set. Eviction order
1132    /// is only rebuilt when the pool is actually over limit.
1133    ///
1134    /// Note: Only `max_txs` is enforced here; `max_size` is intentionally not checked for 2D pools
1135    /// since the protocol pool already enforces size-based limits as a primary defense.
1136    fn discard(&mut self) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1137        let mut removed = Vec::new();
1138
1139        // Compute counts lazily by scanning the pool
1140        let (pending_count, queued_count) = self.pending_and_queued_txn_count();
1141
1142        // Evict queued transactions if over queued limit (lowest priority first)
1143        if queued_count > self.config.queued_limit.max_txs {
1144            let queued_excess = queued_count - self.config.queued_limit.max_txs;
1145            self.evict_lowest_priority(queued_excess, false, &mut removed);
1146        }
1147
1148        // Evict pending transactions if over pending limit (lowest priority first)
1149        if pending_count > self.config.pending_limit.max_txs {
1150            let pending_excess = pending_count - self.config.pending_limit.max_txs;
1151            self.evict_lowest_priority(pending_excess, true, &mut removed);
1152        }
1153
1154        if !removed.is_empty() {
1155            self.metrics.inc_removed(removed.len());
1156        }
1157
1158        removed
1159    }
1160
1161    /// Evicts the lowest-priority transactions from the pool.
1162    ///
1163    /// Scans the single eviction set (ordered by priority) and filters by `is_pending`
1164    /// to find queued or pending transactions to evict. This is a best-effort scan
1165    /// that checks a bool for each transaction.
1166    fn evict_lowest_priority(
1167        &mut self,
1168        count: usize,
1169        evict_pending: bool,
1170        removed: &mut Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
1171    ) {
1172        if count == 0 {
1173            return;
1174        }
1175
1176        removed.reserve(count);
1177
1178        if evict_pending {
1179            // For pending eviction, consider both regular 2D txs and expiring nonce txs
1180            for _ in 0..count {
1181                if let Some(tx) = self.evict_one_pending() {
1182                    removed.push(tx);
1183                } else {
1184                    break;
1185                }
1186            }
1187        } else {
1188            // For queued, only look at by_eviction_order (expiring nonce txs are always pending)
1189            let to_remove: Vec<_> = self
1190                .by_eviction_order
1191                .iter()
1192                .filter(|key| !key.is_pending())
1193                .map(|key| key.tx_id)
1194                .take(count)
1195                .collect();
1196
1197            for id in to_remove {
1198                if let Some(tx) = self.remove_transaction_by_id(&id) {
1199                    removed.push(tx);
1200                }
1201            }
1202        }
1203    }
1204
1205    /// Evicts one pending transaction, considering both regular 2D and expiring nonce txs.
1206    /// Evicts the transaction with lowest priority; ties broken by submission order (newer first).
1207    fn evict_one_pending(&mut self) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1208        let worst_2d = self
1209            .by_eviction_order
1210            .iter()
1211            .find(|key| key.is_pending())
1212            .map(|key| (key.tx_id, key.priority().clone(), key.submission_id()));
1213
1214        let worst_expiring = self
1215            .expiring_nonce_eviction_order
1216            .first()
1217            .map(|key| (key.priority().clone(), key.submission_id()));
1218
1219        match (worst_2d, worst_expiring) {
1220            (Some((id, pri_2d, sid_2d)), Some((pri_exp, sid_exp))) => {
1221                // Same ordering as EvictionKey::Ord: lower priority first, newer first.
1222                let evict_expiring = pri_exp
1223                    .cmp(&pri_2d)
1224                    .then_with(|| sid_2d.cmp(&sid_exp))
1225                    .is_le();
1226                if evict_expiring {
1227                    self.evict_worst_expiring_nonce_tx()
1228                } else {
1229                    self.evict_2d_pending_tx(&id)
1230                }
1231            }
1232            (Some((id, ..)), None) => self.evict_2d_pending_tx(&id),
1233            (None, Some(_)) => self.evict_worst_expiring_nonce_tx(),
1234            (None, None) => None,
1235        }
1236    }
1237
1238    /// Evicts a regular 2D pending transaction by ID.
1239    fn evict_2d_pending_tx(
1240        &mut self,
1241        id: &AA2dTransactionId,
1242    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1243        let tx = self.remove_transaction_by_id(id)?;
1244        self.demote_descendants(id);
1245        Some(tx)
1246    }
1247
1248    /// Evicts the worst expiring nonce transaction.
1249    ///
1250    /// Use when eviction has selected the front of
1251    /// `expiring_nonce_eviction_order`; this pops it directly instead of doing a
1252    /// keyed removal.
1253    fn evict_worst_expiring_nonce_tx(
1254        &mut self,
1255    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1256        let eviction_key = self.expiring_nonce_eviction_order.pop_first()?;
1257        let pending_tx = self
1258            .expiring_nonce_txs
1259            .remove(&eviction_key.expiring_hash())?;
1260
1261        Some(self.remove_expiring_nonce_pending_tx(pending_tx))
1262    }
1263
1264    /// Removes an expiring nonce transaction by hash.
1265    ///
1266    /// Use when removal starts from a hash, such as direct removal, sender
1267    /// removal, or nonce-state inclusion. This path removes the matching
1268    /// eviction key by lookup.
1269    fn remove_expiring_nonce_tx(
1270        &mut self,
1271        expiring_hash: &B256,
1272    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1273        let pending_tx = self.expiring_nonce_txs.remove(expiring_hash)?;
1274        self.expiring_nonce_eviction_order
1275            .remove(&EvictionOrderKey::new(
1276                TempoTipOrdering::default()
1277                    .priority(&pending_tx.transaction.transaction, self.base_fee),
1278                pending_tx.submission_id,
1279            ));
1280        Some(self.remove_expiring_nonce_pending_tx(pending_tx))
1281    }
1282
1283    /// Removes secondary state for an already-detached expiring nonce transaction.
1284    ///
1285    /// Call only after removing the transaction from `expiring_nonce_txs` and
1286    /// handling its eviction-order entry. Shared by the pop-first eviction path
1287    /// and the hash-based removal path.
1288    fn remove_expiring_nonce_pending_tx(
1289        &mut self,
1290        pending_tx: AA2dStoredTransaction,
1291    ) -> Arc<ValidPoolTransaction<TempoPooledTransaction>> {
1292        self.by_hash.remove(pending_tx.transaction.hash());
1293        if let Some(slot) = pending_tx.transaction.transaction.expiring_nonce_slot() {
1294            self.slot_to_expiring_nonce_hash.remove(&slot);
1295        }
1296        self.decrement_sender_count(pending_tx.transaction.sender());
1297        self.pending_count -= 1;
1298        pending_tx.transaction
1299    }
1300
1301    fn remove_from_counts(&mut self, pending: bool) {
1302        if pending {
1303            self.pending_count -= 1;
1304        } else {
1305            self.queued_count -= 1;
1306        }
1307    }
1308
1309    /// Returns a reference to the metrics for this pool
1310    pub fn metrics(&self) -> &AA2dPoolMetrics {
1311        &self.metrics
1312    }
1313
1314    /// Returns `true` if the transaction with the given hash is already included in this pool.
1315    pub(crate) fn contains(&self, tx_hash: &TxHash) -> bool {
1316        self.by_hash.contains_key(tx_hash)
1317    }
1318
1319    /// Returns hashes of transactions in the pool that can be propagated.
1320    pub(crate) fn pooled_transactions_hashes_iter(&self) -> impl Iterator<Item = TxHash> {
1321        self.by_hash
1322            .values()
1323            .filter(|tx| tx.propagate)
1324            .map(|tx| *tx.hash())
1325    }
1326
1327    /// Returns transactions in the pool that can be propagated
1328    pub(crate) fn pooled_transactions_iter(
1329        &self,
1330    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
1331        self.by_hash.values().filter(|tx| tx.propagate).cloned()
1332    }
1333
1334    const fn next_id(&mut self) -> u64 {
1335        let id = self.submission_id;
1336        self.submission_id = self.submission_id.wrapping_add(1);
1337        id
1338    }
1339
1340    /// Caches the 2D nonce key slot for the given sender and nonce key.
1341    fn record_2d_slot(&mut self, transaction: &TempoPooledTransaction) {
1342        let address = transaction.sender();
1343        let nonce_key = transaction.nonce_key().unwrap_or_default();
1344        let Some(slot) = transaction.nonce_key_slot() else {
1345            return;
1346        };
1347
1348        trace!(target: "txpool::2d", ?address, ?nonce_key, "recording 2d nonce slot");
1349        let seq_id = AASequenceId::new(address, nonce_key);
1350
1351        if self.slot_to_seq_id.insert(slot, seq_id).is_none() {
1352            self.metrics.inc_nonce_key_count(1);
1353        }
1354    }
1355
1356    /// Processes nonce-precompile storage updates and updates internal state accordingly.
1357    pub(crate) fn on_state_updates(
1358        &mut self,
1359        state: &AddressMap<BundleAccount>,
1360    ) -> PoolUpdateResult {
1361        self.state_update_nonce_changes.clear();
1362        self.state_update_included_expiring_nonce_hashes.clear();
1363
1364        let Some(nonce_state) = state.get(&NONCE_PRECOMPILE_ADDRESS) else {
1365            return (Vec::new(), Vec::new());
1366        };
1367
1368        let mut changes = std::mem::take(&mut self.state_update_nonce_changes);
1369        let mut included_expiring_nonce_hashes =
1370            std::mem::take(&mut self.state_update_included_expiring_nonce_hashes);
1371
1372        // Process known 2D nonce slot changes.
1373        for (slot, value) in nonce_state.storage.iter() {
1374            if let Some(seq_id) = self.slot_to_seq_id.get(slot) {
1375                changes.insert(*seq_id, value.present_value.saturating_to());
1376            }
1377            // Detect included expiring nonce transactions via their
1378            // `expiring_nonce_seen` slot being set to a non-zero value.
1379            if !value.present_value.is_zero()
1380                && let Some(expiring_nonce_hash) = self.slot_to_expiring_nonce_hash.get(slot)
1381            {
1382                included_expiring_nonce_hashes.push(*expiring_nonce_hash);
1383            }
1384        }
1385
1386        let (promoted, mut mined) = self.on_nonce_changes_iter(changes.drain());
1387
1388        // Remove included expiring nonce transactions
1389        for expiring_nonce_hash in included_expiring_nonce_hashes.drain(..) {
1390            if let Some(tx) = self.remove_expiring_nonce_tx(&expiring_nonce_hash) {
1391                mined.push(tx);
1392            }
1393        }
1394        self.state_update_nonce_changes = changes;
1395        self.state_update_included_expiring_nonce_hashes = included_expiring_nonce_hashes;
1396
1397        // Record metrics for all changes
1398        if !promoted.is_empty() {
1399            self.metrics.inc_promoted(promoted.len());
1400        }
1401        if !mined.is_empty() {
1402            self.metrics.inc_removed(mined.len());
1403        }
1404        self.update_metrics();
1405
1406        (promoted, mined)
1407    }
1408
1409    /// Asserts that all assumptions are valid.
1410    #[cfg(test)]
1411    pub(crate) fn assert_invariants(&self) {
1412        // Basic size constraints
1413        assert!(
1414            self.independent_transactions.len() <= self.by_id.len(),
1415            "independent_transactions.len() ({}) > by_id.len() ({})",
1416            self.independent_transactions.len(),
1417            self.by_id.len()
1418        );
1419        // by_hash contains both regular 2D nonce txs (in by_id) and expiring nonce txs
1420        assert_eq!(
1421            self.by_id.len() + self.expiring_nonce_txs.len(),
1422            self.by_hash.len(),
1423            "by_id.len() ({}) + expiring_nonce_txs.len() ({}) != by_hash.len() ({})",
1424            self.by_id.len(),
1425            self.expiring_nonce_txs.len(),
1426            self.by_hash.len()
1427        );
1428        assert_eq!(
1429            self.expiring_nonce_txs.len(),
1430            self.expiring_nonce_eviction_order.len(),
1431            "expiring_nonce_txs.len() ({}) != expiring_nonce_eviction_order.len() ({})",
1432            self.expiring_nonce_txs.len(),
1433            self.expiring_nonce_eviction_order.len()
1434        );
1435        assert_eq!(
1436            self.by_id.len(),
1437            self.by_eviction_order.len(),
1438            "by_id.len() ({}) != by_eviction_order.len() ({})",
1439            self.by_id.len(),
1440            self.by_eviction_order.len()
1441        );
1442
1443        // All independent transactions must exist in by_id
1444        for (seq_id, independent_tx) in &self.independent_transactions {
1445            let tx_id = independent_tx
1446                .transaction
1447                .transaction
1448                .aa_transaction_id()
1449                .expect("Independent transaction must have AA transaction ID");
1450            assert!(
1451                self.by_id.contains_key(&tx_id),
1452                "Independent transaction {tx_id:?} not in by_id"
1453            );
1454            assert_eq!(
1455                seq_id, &tx_id.seq_id,
1456                "Independent transactions sequence ID {seq_id:?} does not match transaction sequence ID {tx_id:?}"
1457            );
1458
1459            // Independent transactions must be pending
1460            let tx_in_pool = self.by_id.get(&tx_id).unwrap();
1461            assert!(
1462                tx_in_pool.is_pending(),
1463                "Independent transaction {tx_id:?} is not pending"
1464            );
1465
1466            // Independent transaction should match the one in by_id
1467            assert_eq!(
1468                independent_tx.transaction.hash(),
1469                tx_in_pool.inner.transaction.hash(),
1470                "Independent transaction hash mismatch for {tx_id:?}"
1471            );
1472        }
1473
1474        // Each sender should have at most one transaction in independent set
1475        let mut seen_senders = std::collections::HashSet::new();
1476        for id in self.independent_transactions.keys() {
1477            assert!(
1478                seen_senders.insert(*id),
1479                "Duplicate sender {id:?} in independent transactions"
1480            );
1481        }
1482
1483        // Verify by_hash integrity
1484        for (hash, tx) in &self.by_hash {
1485            // Hash should match transaction hash
1486            assert_eq!(
1487                tx.hash(),
1488                hash,
1489                "Hash mismatch in by_hash: expected {:?}, got {:?}",
1490                hash,
1491                tx.hash()
1492            );
1493
1494            // Expiring nonce txs are stored in expiring_nonce_txs, not by_id
1495            if tx.transaction.is_expiring_nonce() {
1496                assert!(
1497                    self.expiring_nonce_txs
1498                        .contains_key(&tx.transaction.precomputed_expiring_nonce_hash()),
1499                    "Expiring nonce transaction with hash {hash:?} in by_hash but not in expiring_nonce_txs"
1500                );
1501                continue;
1502            }
1503
1504            // Transaction in by_hash should exist in by_id
1505            let id = tx
1506                .transaction
1507                .aa_transaction_id()
1508                .expect("Transaction in pool should be AA transaction");
1509            assert!(
1510                self.by_id.contains_key(&id),
1511                "Transaction with hash {hash:?} in by_hash but not in by_id"
1512            );
1513
1514            // The transaction in by_id should have the same hash
1515            let tx_in_by_id = &self.by_id.get(&id).unwrap().inner.transaction;
1516            assert_eq!(
1517                tx.hash(),
1518                tx_in_by_id.hash(),
1519                "Transaction hash mismatch between by_hash and by_id for id {id:?}"
1520            );
1521        }
1522
1523        // Verify by_id integrity
1524        for (id, tx) in &self.by_id {
1525            // Transaction in by_id should exist in by_hash
1526            let hash = tx.inner.transaction.hash();
1527            assert!(
1528                self.by_hash.contains_key(hash),
1529                "Transaction with id {id:?} in by_id but not in by_hash"
1530            );
1531
1532            // The transaction should have the correct AA ID
1533            let tx_id = tx
1534                .inner
1535                .transaction
1536                .transaction
1537                .aa_transaction_id()
1538                .expect("Transaction in pool should be AA transaction");
1539            assert_eq!(
1540                &tx_id, id,
1541                "Transaction ID mismatch: expected {id:?}, got {tx_id:?}"
1542            );
1543
1544            // If THIS transaction is the independent transaction for its sequence, it must be pending
1545            if let Some(independent_tx) = self.independent_transactions.get(&id.seq_id)
1546                && independent_tx.transaction.hash() == tx.inner.transaction.hash()
1547            {
1548                assert!(
1549                    tx.is_pending(),
1550                    "Transaction {id:?} is in independent set but not pending"
1551                );
1552            }
1553
1554            let expected_priority = TempoTipOrdering::default()
1555                .priority(&tx.inner.transaction.transaction, self.base_fee);
1556            let expected_order =
1557                EvictionOrderKey::new(expected_priority.clone(), tx.inner.submission_id);
1558            let Some(eviction_key) = self.by_eviction_order.get(&expected_order) else {
1559                panic!("Transaction with id {id:?} in by_id but not in by_eviction_order");
1560            };
1561            assert_eq!(
1562                eviction_key.tx_id, *id,
1563                "Eviction key for transaction {id:?} has mismatched transaction id"
1564            );
1565            assert_eq!(
1566                eviction_key.priority(),
1567                &expected_priority,
1568                "Eviction key for transaction {id:?} has stale priority"
1569            );
1570            assert_eq!(
1571                eviction_key.tx.inner.transaction.hash(),
1572                tx.inner.transaction.hash(),
1573                "Eviction key for transaction {id:?} has mismatched transaction hash"
1574            );
1575        }
1576
1577        for key in &self.by_eviction_order {
1578            let Some(tx) = self.by_id.get(&key.tx_id) else {
1579                panic!("Eviction key {:?} not in by_id", key.tx_id);
1580            };
1581            assert_eq!(
1582                key.submission_id(),
1583                tx.inner.submission_id,
1584                "Eviction key {:?} has mismatched submission id",
1585                key.tx_id
1586            );
1587            let expected_priority = TempoTipOrdering::default()
1588                .priority(&tx.inner.transaction.transaction, self.base_fee);
1589            assert_eq!(
1590                key.priority(),
1591                &expected_priority,
1592                "Eviction key {:?} has stale priority",
1593                key.tx_id
1594            );
1595            assert_eq!(
1596                key.tx.inner.transaction.hash(),
1597                tx.inner.transaction.hash(),
1598                "Eviction key {:?} has mismatched transaction hash",
1599                key.tx_id
1600            );
1601        }
1602
1603        // Verify pending/queued consistency
1604        // pending_and_queued_txn_count includes expiring nonce txs in pending count
1605        let (pending_count, queued_count) = self.pending_and_queued_txn_count();
1606        let (scanned_pending, scanned_queued) = self.scan_pending_and_queued_txn_count();
1607        assert_eq!(
1608            (pending_count, queued_count),
1609            (scanned_pending, scanned_queued),
1610            "cached counts ({pending_count}, {queued_count}) != scanned counts ({scanned_pending}, {scanned_queued})"
1611        );
1612        assert_eq!(
1613            pending_count + queued_count,
1614            self.by_id.len() + self.expiring_nonce_txs.len(),
1615            "Pending ({}) + queued ({}) != total transactions (by_id: {} + expiring: {})",
1616            pending_count,
1617            queued_count,
1618            self.by_id.len(),
1619            self.expiring_nonce_txs.len()
1620        );
1621
1622        // Verify quota compliance - counts don't exceed limits
1623        assert!(
1624            pending_count <= self.config.pending_limit.max_txs,
1625            "pending_count {} exceeds limit {}",
1626            pending_count,
1627            self.config.pending_limit.max_txs
1628        );
1629        assert!(
1630            queued_count <= self.config.queued_limit.max_txs,
1631            "queued_count {} exceeds limit {}",
1632            queued_count,
1633            self.config.queued_limit.max_txs
1634        );
1635
1636        // Verify expiring nonce txs integrity
1637        for (hash, pending_tx) in &self.expiring_nonce_txs {
1638            let tx_hash = *pending_tx.transaction.hash();
1639            assert!(
1640                self.by_hash.contains_key(&tx_hash),
1641                "Expiring nonce tx {tx_hash:?} not in by_hash (expiring hash {hash:?})"
1642            );
1643            assert!(
1644                self.expiring_nonce_eviction_order
1645                    .iter()
1646                    .any(|key| key.expiring_hash() == *hash
1647                        && key.submission_id() == pending_tx.submission_id),
1648                "Expiring nonce tx {tx_hash:?} not in expiring_nonce_eviction_order"
1649            );
1650            assert!(
1651                pending_tx.transaction.transaction.is_expiring_nonce(),
1652                "Transaction in expiring_nonce_txs is not an expiring nonce tx"
1653            );
1654        }
1655
1656        for key in &self.expiring_nonce_eviction_order {
1657            let expiring_hash = key.expiring_hash();
1658            let Some(pending_tx) = self.expiring_nonce_txs.get(&expiring_hash) else {
1659                panic!("Expiring nonce eviction key {expiring_hash:?} not in expiring_nonce_txs");
1660            };
1661            assert_eq!(
1662                key.submission_id(),
1663                pending_tx.submission_id,
1664                "Expiring nonce eviction key {expiring_hash:?} has mismatched submission id"
1665            );
1666            assert_eq!(
1667                key.transaction.hash(),
1668                pending_tx.transaction.hash(),
1669                "Expiring nonce eviction key {expiring_hash:?} has mismatched transaction hash"
1670            );
1671        }
1672    }
1673}
1674
1675/// Default maximum number of transactions per sender in the AA 2D pool.
1676///
1677/// This limit prevents a single sender from monopolizing pool capacity.
1678pub const DEFAULT_MAX_TXS_PER_SENDER: usize = 16;
1679
1680/// Settings for the [`AA2dPoolConfig`]
1681#[derive(Debug, Clone)]
1682pub struct AA2dPoolConfig {
1683    /// Price bump (in %) for the transaction pool underpriced check.
1684    pub price_bump_config: PriceBumpConfig,
1685    /// Maximum number of pending (executable) transactions
1686    pub pending_limit: SubPoolLimit,
1687    /// Maximum number of queued (non-executable) transactions
1688    pub queued_limit: SubPoolLimit,
1689    /// Maximum number of transactions per sender.
1690    ///
1691    /// Prevents a single sender from monopolizing pool capacity (DoS protection).
1692    pub max_txs_per_sender: usize,
1693}
1694
1695impl Default for AA2dPoolConfig {
1696    fn default() -> Self {
1697        Self {
1698            price_bump_config: PriceBumpConfig::default(),
1699            pending_limit: SubPoolLimit::default(),
1700            queued_limit: SubPoolLimit::default(),
1701            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
1702        }
1703    }
1704}
1705
1706#[derive(Debug, Clone)]
1707struct AA2dStoredTransaction {
1708    submission_id: u64,
1709    transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
1710}
1711
1712impl AA2dStoredTransaction {
1713    fn new(
1714        submission_id: u64,
1715        transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
1716    ) -> Self {
1717        Self {
1718            submission_id,
1719            transaction,
1720        }
1721    }
1722
1723    fn clone_into_pending(&self, base_fee: u64) -> PendingTransaction<TxOrdering> {
1724        PendingTransaction {
1725            submission_id: self.submission_id,
1726            priority: TempoTipOrdering::default().priority(&self.transaction.transaction, base_fee),
1727            transaction: self.transaction.clone(),
1728        }
1729    }
1730}
1731
1732#[derive(Debug)]
1733struct AA2dInternalTransaction {
1734    /// Keeps track of the transaction without an ordering priority.
1735    inner: AA2dStoredTransaction,
1736    /// Whether this transaction is pending/executable.
1737    ///
1738    /// If it's not pending, it is queued.
1739    ///
1740    /// Uses `AtomicBool` so we can mutate this flag without removing/reinserting
1741    /// the transaction from the eviction set. This allows a single eviction set for
1742    /// all transactions, with pending/queued filtering done at eviction time.
1743    is_pending: AtomicBool,
1744}
1745
1746impl AA2dInternalTransaction {
1747    /// Returns whether this transaction is pending/executable.
1748    fn is_pending(&self) -> bool {
1749        self.is_pending.load(Ordering::Relaxed)
1750    }
1751
1752    /// Sets the pending status of this transaction, returning the previous value.
1753    fn set_pending(&self, pending: bool) -> bool {
1754        self.is_pending.swap(pending, Ordering::Relaxed)
1755    }
1756}
1757
1758/// Minimal ordering key for eviction set lookups.
1759///
1760/// [`EvictionKey`] and [`ExpiringNonceEvictionKey`] borrow as this type so BTreeSet
1761/// lookup APIs like `remove`, `contains`, and `get` do not need to construct a full
1762/// key with a cloned transaction.
1763#[derive(Debug, Clone, PartialEq, Eq)]
1764struct EvictionOrderKey {
1765    priority: Priority<u64>,
1766    submission_id: u64,
1767}
1768
1769impl EvictionOrderKey {
1770    fn new(priority: Priority<u64>, submission_id: u64) -> Self {
1771        Self {
1772            priority,
1773            submission_id,
1774        }
1775    }
1776}
1777
1778impl Ord for EvictionOrderKey {
1779    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1780        self.priority
1781            .cmp(&other.priority)
1782            .then_with(|| other.submission_id.cmp(&self.submission_id))
1783    }
1784}
1785
1786impl PartialOrd for EvictionOrderKey {
1787    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1788        Some(self.cmp(other))
1789    }
1790}
1791
1792/// Ordering key for `AA2dPool::expiring_nonce_eviction_order`.
1793///
1794/// This mirrors `EvictionKey` for expiring nonce transactions, which are not
1795/// stored in `by_id` and therefore cannot use `AA2dTransactionId`. The key
1796/// carries the transaction so `BestAA2dTransactions` can clone only the ordered
1797/// index and pop expiring nonce transactions directly from it.
1798///
1799/// Order:
1800/// 1. Priority ascending (lowest priority evicted first)
1801/// 2. Submission ID descending (newer transactions evicted first among equal priority)
1802///
1803/// `submission_id` is unique for live entries.
1804#[derive(Debug, Clone)]
1805struct ExpiringNonceEvictionKey {
1806    order: EvictionOrderKey,
1807    transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
1808}
1809
1810impl ExpiringNonceEvictionKey {
1811    fn from_pending_with_base_fee(tx: &AA2dStoredTransaction, base_fee: u64) -> Self {
1812        Self {
1813            order: EvictionOrderKey::new(
1814                TempoTipOrdering::default().priority(&tx.transaction.transaction, base_fee),
1815                tx.submission_id,
1816            ),
1817            transaction: tx.transaction.clone(),
1818        }
1819    }
1820
1821    fn from_pending_owned(tx: PendingTransaction<TxOrdering>) -> Self {
1822        Self {
1823            order: EvictionOrderKey::new(tx.priority, tx.submission_id),
1824            transaction: tx.transaction,
1825        }
1826    }
1827
1828    fn into_transaction(self) -> PendingTransaction<TxOrdering> {
1829        PendingTransaction {
1830            submission_id: self.order.submission_id,
1831            priority: self.order.priority,
1832            transaction: self.transaction,
1833        }
1834    }
1835
1836    fn priority(&self) -> &Priority<u64> {
1837        &self.order.priority
1838    }
1839
1840    fn submission_id(&self) -> u64 {
1841        self.order.submission_id
1842    }
1843
1844    fn expiring_hash(&self) -> B256 {
1845        self.transaction
1846            .transaction
1847            .precomputed_expiring_nonce_hash()
1848    }
1849}
1850
1851impl Borrow<EvictionOrderKey> for ExpiringNonceEvictionKey {
1852    fn borrow(&self) -> &EvictionOrderKey {
1853        &self.order
1854    }
1855}
1856
1857impl PartialEq for ExpiringNonceEvictionKey {
1858    fn eq(&self, other: &Self) -> bool {
1859        self.order == other.order
1860    }
1861}
1862
1863impl Eq for ExpiringNonceEvictionKey {}
1864
1865impl Ord for ExpiringNonceEvictionKey {
1866    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1867        self.order.cmp(&other.order)
1868    }
1869}
1870
1871impl PartialOrd for ExpiringNonceEvictionKey {
1872    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1873        Some(self.cmp(other))
1874    }
1875}
1876
1877/// Key for ordering transactions by eviction priority.
1878///
1879/// Orders by:
1880/// 1. Priority ascending (lowest priority evicted first)
1881/// 2. Submission ID descending (newer transactions evicted first among same priority)
1882///
1883/// This is the inverse of the execution order (where highest priority, oldest submission wins).
1884/// Newer transactions are evicted first to preserve older transactions that have been waiting longer.
1885#[derive(Debug, Clone)]
1886struct EvictionKey {
1887    /// The wrapped transaction, used to read live pending/queued status.
1888    tx: Arc<AA2dInternalTransaction>,
1889    /// The transaction's unique identifier (cached for lookup during eviction).
1890    /// We cache this because deriving it from the transaction requires
1891    /// `aa_transaction_id()` which returns an Option and does more work.
1892    tx_id: AA2dTransactionId,
1893    /// Priority and submission ID snapshot used for eviction ordering.
1894    order: EvictionOrderKey,
1895}
1896
1897impl EvictionKey {
1898    fn with_base_fee(
1899        tx: Arc<AA2dInternalTransaction>,
1900        tx_id: AA2dTransactionId,
1901        base_fee: u64,
1902    ) -> Self {
1903        let priority =
1904            TempoTipOrdering::default().priority(&tx.inner.transaction.transaction, base_fee);
1905        let submission_id = tx.inner.submission_id;
1906        Self {
1907            tx,
1908            tx_id,
1909            order: EvictionOrderKey::new(priority, submission_id),
1910        }
1911    }
1912
1913    /// Returns the transaction's priority.
1914    fn priority(&self) -> &Priority<u64> {
1915        &self.order.priority
1916    }
1917
1918    /// Returns the submission ID.
1919    fn submission_id(&self) -> u64 {
1920        self.order.submission_id
1921    }
1922
1923    /// Returns whether this transaction is pending.
1924    fn is_pending(&self) -> bool {
1925        self.tx.is_pending()
1926    }
1927}
1928
1929impl Borrow<EvictionOrderKey> for EvictionKey {
1930    fn borrow(&self) -> &EvictionOrderKey {
1931        &self.order
1932    }
1933}
1934
1935impl PartialEq for EvictionKey {
1936    fn eq(&self, other: &Self) -> bool {
1937        self.submission_id() == other.submission_id()
1938    }
1939}
1940
1941impl Eq for EvictionKey {}
1942
1943impl Ord for EvictionKey {
1944    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1945        // Lower priority first (evict lowest priority)
1946        self.priority()
1947            .cmp(other.priority())
1948            // Then newer submission first (evict newer transactions among same priority)
1949            // This preserves older transactions that have been waiting longer
1950            .then_with(|| other.submission_id().cmp(&self.submission_id()))
1951    }
1952}
1953
1954impl PartialOrd for EvictionKey {
1955    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1956        Some(self.cmp(other))
1957    }
1958}
1959
1960/// Maximum number of new transactions to drain from the channel per `next()` call.
1961const MAX_NEW_TRANSACTIONS_PER_BATCH: usize = 16;
1962
1963/// Determines how a newly received transaction should be handled based on its priority
1964/// relative to transactions already yielded by the iterator.
1965enum IncomingAA2dTransaction {
1966    /// Priority ≤ last yielded — safe to add to both `by_id` and `independent`.
1967    Process(PendingTransaction<TxOrdering>),
1968    /// Priority > last yielded — add only to `by_id` for nonce chain lookups, not `independent`.
1969    Stash(PendingTransaction<TxOrdering>),
1970}
1971
1972enum PoppedAA2dTransaction {
1973    Regular(AA2dTransactionId, PendingTransaction<TxOrdering>),
1974    Expiring(PendingTransaction<TxOrdering>),
1975}
1976
1977/// A snapshot of the sub-pool containing all executable transactions.
1978#[derive(Debug)]
1979pub(crate) struct BestAA2dTransactions {
1980    /// Regular 2D nonce independent pending transactions sorted by their priority.
1981    ///
1982    /// Expiring nonce transactions are not included here because they are keyed
1983    /// by expiring nonce hash, not `AA2dTransactionId`.
1984    independent: BTreeSet<PendingTransaction<TxOrdering>>,
1985    /// Regular 2D nonce pending transactions grouped by their unique identifier.
1986    ///
1987    /// Expiring nonce transactions are not stored in `by_id`; they are tracked
1988    /// separately by `expiring_nonce_order`.
1989    by_id: HashMap<AA2dTransactionId, AA2dStoredTransaction>,
1990    /// Expiring nonce pending transactions in eviction order. The best
1991    /// transaction is at the back of the set, and the key carries the pending
1992    /// transaction so this snapshot does not clone the pool's expiring hash map.
1993    expiring_nonce_order: BTreeSet<ExpiringNonceEvictionKey>,
1994
1995    /// There might be the case where a yielded transactions is invalid, this will track it.
1996    invalid: HashSet<AASequenceId>,
1997    /// Live feed of new pending transactions arriving after this iterator was created.
1998    new_transaction_receiver: Option<broadcast::Receiver<AA2dStoredTransaction>>,
1999    /// Priority of the most recently yielded transaction, used to maintain ordering invariant.
2000    last_priority: Option<Priority<u64>>,
2001    /// Base fee used to filter and prioritize this block-building snapshot.
2002    base_fee: u64,
2003}
2004
2005impl BestAA2dTransactions {
2006    /// Removes the best regular transaction from the set.
2007    fn pop_best_regular(&mut self) -> Option<(AA2dTransactionId, PendingTransaction<TxOrdering>)> {
2008        let tx = self.independent.pop_last()?;
2009        let id = tx
2010            .transaction
2011            .transaction
2012            .aa_transaction_id()
2013            .expect("Transaction in AA2D pool must be an AA transaction with valid nonce key");
2014        self.by_id.remove(&id);
2015        Some((id, tx))
2016    }
2017
2018    /// Removes the best expiring nonce transaction from the set.
2019    fn pop_best_expiring_nonce(&mut self) -> Option<PendingTransaction<TxOrdering>> {
2020        let key = self.expiring_nonce_order.pop_last()?;
2021        Some(key.into_transaction())
2022    }
2023
2024    /// Removes the best regular or expiring nonce transaction.
2025    fn pop_best(&mut self) -> Option<PoppedAA2dTransaction> {
2026        match (self.independent.last(), self.expiring_nonce_order.last()) {
2027            (Some(regular), Some(expiring)) => {
2028                if regular
2029                    .priority
2030                    .cmp(expiring.priority())
2031                    .then_with(|| expiring.submission_id().cmp(&regular.submission_id))
2032                    .is_ge()
2033                {
2034                    let (id, tx) = self.pop_best_regular()?;
2035                    Some(PoppedAA2dTransaction::Regular(id, tx))
2036                } else {
2037                    self.pop_best_expiring_nonce()
2038                        .map(PoppedAA2dTransaction::Expiring)
2039                }
2040            }
2041            (Some(_), None) => {
2042                let (id, tx) = self.pop_best_regular()?;
2043                Some(PoppedAA2dTransaction::Regular(id, tx))
2044            }
2045            (None, Some(_)) => self
2046                .pop_best_expiring_nonce()
2047                .map(PoppedAA2dTransaction::Expiring),
2048            (None, None) => None,
2049        }
2050    }
2051
2052    /// Non-blocking read on the new pending transactions subscription channel.
2053    fn try_recv(&mut self) -> Option<IncomingAA2dTransaction> {
2054        loop {
2055            match self.new_transaction_receiver.as_mut()?.try_recv() {
2056                Ok(tx) => {
2057                    let priority = TempoTipOrdering::default()
2058                        .priority(&tx.transaction.transaction, self.base_fee);
2059                    let tx = PendingTransaction {
2060                        submission_id: tx.submission_id,
2061                        transaction: tx.transaction,
2062                        priority,
2063                    };
2064                    if let Some(last_priority) = &self.last_priority
2065                        && &tx.priority > last_priority
2066                    {
2067                        // Higher priority than what we already yielded — stash in `by_id`
2068                        // only (not `independent`) to preserve nonce chain lookups.
2069                        return Some(IncomingAA2dTransaction::Stash(tx));
2070                    }
2071                    return Some(IncomingAA2dTransaction::Process(tx));
2072                }
2073                Err(broadcast::error::TryRecvError::Lagged(_)) => {
2074                    // Buffer overflowed; self-corrects on next call.
2075                }
2076                Err(_) => return None,
2077            }
2078        }
2079    }
2080
2081    /// Drains new pending transactions from the broadcast channel and inserts them.
2082    fn add_new_transactions(&mut self) {
2083        for _ in 0..MAX_NEW_TRANSACTIONS_PER_BATCH {
2084            if let Some(incoming) = self.try_recv() {
2085                let (tx, process) = match incoming {
2086                    IncomingAA2dTransaction::Process(tx) => (tx, true),
2087                    IncomingAA2dTransaction::Stash(tx) => (tx, false),
2088                };
2089                if tx.transaction.transaction.is_expiring_nonce() {
2090                    if process && can_pay_base_fee(&tx, self.base_fee) {
2091                        self.expiring_nonce_order
2092                            .insert(ExpiringNonceEvictionKey::from_pending_owned(tx));
2093                    }
2094                } else if let Some(id) = tx.transaction.transaction.aa_transaction_id() {
2095                    if process {
2096                        // Only mark as independent if no ancestor is already tracked
2097                        if !self.by_id.contains_key(&AA2dTransactionId::new(
2098                            id.seq_id,
2099                            id.nonce.saturating_sub(1),
2100                        )) || id.nonce == 0
2101                        {
2102                            self.independent.insert(tx.clone());
2103                        }
2104                    }
2105                    self.by_id.insert(
2106                        id,
2107                        AA2dStoredTransaction {
2108                            submission_id: tx.submission_id,
2109                            transaction: tx.transaction,
2110                        },
2111                    );
2112                }
2113            } else {
2114                break;
2115            }
2116        }
2117    }
2118
2119    /// Returns the next best transaction with its priority.
2120    pub(crate) fn next_tx_and_priority(
2121        &mut self,
2122    ) -> Option<(
2123        Arc<ValidPoolTransaction<TempoPooledTransaction>>,
2124        Priority<u64>,
2125    )> {
2126        loop {
2127            self.add_new_transactions();
2128            let best = match self.pop_best()? {
2129                PoppedAA2dTransaction::Regular(id, best) => {
2130                    if self.invalid.contains(&id.seq_id) {
2131                        continue;
2132                    }
2133                    if !can_pay_base_fee(&best, self.base_fee) {
2134                        self.invalid.insert(id.seq_id);
2135                        continue;
2136                    }
2137                    // Advance transaction that just got unlocked, if any.
2138                    if let Some(unlocked) = self.by_id.get(&id.unlocks()) {
2139                        self.independent
2140                            .insert(unlocked.clone_into_pending(self.base_fee));
2141                    }
2142                    best
2143                }
2144                PoppedAA2dTransaction::Expiring(best) => {
2145                    if !can_pay_base_fee(&best, self.base_fee) {
2146                        continue;
2147                    }
2148                    best
2149                }
2150            };
2151            if self.new_transaction_receiver.is_some() {
2152                self.last_priority = Some(best.priority.clone());
2153            }
2154            return Some((best.transaction, best.priority));
2155        }
2156    }
2157}
2158
2159fn can_pay_base_fee(tx: &PendingTransaction<TxOrdering>, base_fee: u64) -> bool {
2160    tx.transaction.transaction.max_fee_per_gas() >= u128::from(base_fee)
2161}
2162
2163impl Iterator for BestAA2dTransactions {
2164    type Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>;
2165
2166    fn next(&mut self) -> Option<Self::Item> {
2167        self.next_tx_and_priority().map(|(tx, _)| tx)
2168    }
2169
2170    fn size_hint(&self) -> (usize, Option<usize>) {
2171        (
2172            0,
2173            self.by_id
2174                .len()
2175                .checked_add(self.expiring_nonce_order.len()),
2176        )
2177    }
2178}
2179
2180impl BestTransactions for BestAA2dTransactions {
2181    fn mark_invalid(&mut self, transaction: &Self::Item, _kind: InvalidPoolTransactionError) {
2182        // Skip invalidation for expiring nonce transactions - they are independent
2183        // and should not block other expiring nonce txs from the same sender
2184        if transaction.transaction.is_expiring_nonce() {
2185            return;
2186        }
2187
2188        if let Some(id) = transaction.transaction.aa_transaction_id() {
2189            self.invalid.insert(id.seq_id);
2190        }
2191    }
2192
2193    fn no_updates(&mut self) {
2194        self.new_transaction_receiver.take();
2195        self.last_priority.take();
2196    }
2197
2198    fn set_skip_blobs(&mut self, _skip_blobs: bool) {}
2199}
2200
2201/// Key for identifying a unique sender sequence in 2D nonce system.
2202///
2203/// This combines the sender address with its nonce key, which
2204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
2205pub struct AASequenceId {
2206    /// The sender address.
2207    pub address: Address,
2208    /// The nonce key for 2D nonce transactions.
2209    pub nonce_key: U256,
2210}
2211
2212impl AASequenceId {
2213    /// Creates a new instance with the address and nonce key.
2214    pub const fn new(address: Address, nonce_key: U256) -> Self {
2215        Self { address, nonce_key }
2216    }
2217
2218    const fn start_bound(self) -> std::ops::Bound<AA2dTransactionId> {
2219        std::ops::Bound::Included(AA2dTransactionId::new(self, 0))
2220    }
2221
2222    /// Returns a range of transactions for this sequence.
2223    const fn range(self) -> std::ops::RangeInclusive<AA2dTransactionId> {
2224        AA2dTransactionId::new(self, 0)..=AA2dTransactionId::new(self, u64::MAX)
2225    }
2226}
2227
2228/// Unique identifier for an AA transaction.
2229///
2230/// Identified by its sender, nonce key and nonce for that nonce key.
2231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
2232pub struct AA2dTransactionId {
2233    /// Uniquely identifies the accounts nonce key sequence
2234    pub(crate) seq_id: AASequenceId,
2235    /// The nonce in that sequence
2236    pub(crate) nonce: u64,
2237}
2238
2239impl AA2dTransactionId {
2240    /// Creates a new identifier.
2241    pub(crate) const fn new(seq_id: AASequenceId, nonce: u64) -> Self {
2242        Self { seq_id, nonce }
2243    }
2244
2245    /// Returns the next transaction in the sequence.
2246    pub(crate) fn unlocks(&self) -> Self {
2247        Self::new(self.seq_id, self.nonce.saturating_add(1))
2248    }
2249
2250    /// Returns the nonce key sequence of this transaction.
2251    pub fn seq_id(&self) -> &AASequenceId {
2252        &self.seq_id
2253    }
2254}
2255
2256#[cfg(test)]
2257mod tests {
2258    use super::*;
2259    use crate::test_utils::{TxBuilder, wrap_valid_tx};
2260    use alloy_eips::eip2930::AccessList;
2261    use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
2262    use reth_primitives_traits::Recovered;
2263    use reth_transaction_pool::PoolTransaction;
2264    use std::collections::HashSet;
2265    use tempo_chainspec::{hardfork::TempoHardfork, spec::TEMPO_T1_BASE_FEE};
2266    use tempo_primitives::{
2267        TempoTxEnvelope,
2268        transaction::{
2269            TempoTransaction,
2270            tempo_transaction::Call,
2271            tt_signature::{PrimitiveSignature, TempoSignature},
2272            tt_signed::AASigned,
2273        },
2274    };
2275
2276    #[test_case::test_case(U256::ZERO)]
2277    #[test_case::test_case(U256::random())]
2278    fn insert_pending(nonce_key: U256) {
2279        let mut pool = AA2dPool::default();
2280
2281        // Set up a sender with a tracked nonce key
2282        let sender = Address::random();
2283
2284        // Create a transaction with nonce_key=1, nonce=0 (should be pending)
2285        let tx = TxBuilder::aa(sender).nonce_key(nonce_key).build();
2286        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
2287
2288        // Add the transaction to the pool
2289        let result = pool.add_transaction(Arc::new(valid_tx), 0, TempoHardfork::T1);
2290
2291        // Should be added as pending
2292        assert!(result.is_ok(), "Transaction should be added successfully");
2293        let added = result.unwrap();
2294        assert!(
2295            matches!(added, AddedTransaction::Pending(_)),
2296            "Transaction should be pending, got: {added:?}"
2297        );
2298
2299        // Verify pool state
2300        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2301        assert_eq!(pending_count, 1, "Should have 1 pending transaction");
2302        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
2303
2304        pool.assert_invariants();
2305    }
2306
2307    #[test_case::test_case(U256::ZERO)]
2308    #[test_case::test_case(U256::random())]
2309    fn insert_with_nonce_gap_then_fill(nonce_key: U256) {
2310        let mut pool = AA2dPool::default();
2311
2312        // Set up a sender with a tracked nonce key
2313        let sender = Address::random();
2314
2315        // Step 1: Insert transaction with nonce=1 (creates a gap, should be queued)
2316        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
2317        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
2318        let tx1_hash = *valid_tx1.hash();
2319
2320        let result1 = pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1);
2321
2322        // Should be queued due to nonce gap
2323        assert!(
2324            result1.is_ok(),
2325            "Transaction 1 should be added successfully"
2326        );
2327        let added1 = result1.unwrap();
2328        assert!(
2329            matches!(
2330                added1,
2331                AddedTransaction::Parked {
2332                    subpool: SubPool::Queued,
2333                    ..
2334                }
2335            ),
2336            "Transaction 1 should be queued due to nonce gap, got: {added1:?}"
2337        );
2338
2339        // Verify pool state after first insert
2340        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2341        assert_eq!(pending_count, 0, "Should have 0 pending transactions");
2342        assert_eq!(queued_count, 1, "Should have 1 queued transaction");
2343
2344        // Verify tx1 is NOT in independent set
2345        let seq_id = AASequenceId::new(sender, nonce_key);
2346        let tx1_id = AA2dTransactionId::new(seq_id, 1);
2347        assert!(
2348            !pool.independent_transactions.contains_key(&tx1_id.seq_id),
2349            "Transaction 1 should not be in independent set yet"
2350        );
2351
2352        pool.assert_invariants();
2353
2354        // Step 2: Insert transaction with nonce=0 (fills the gap)
2355        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
2356        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
2357        let tx0_hash = *valid_tx0.hash();
2358
2359        let result0 = pool.add_transaction(Arc::new(valid_tx0), 0, TempoHardfork::T1);
2360
2361        // Should be pending and promote tx1
2362        assert!(
2363            result0.is_ok(),
2364            "Transaction 0 should be added successfully"
2365        );
2366        let added0 = result0.unwrap();
2367
2368        // Verify it's pending and promoted tx1
2369        match added0 {
2370            AddedTransaction::Pending(ref pending) => {
2371                assert_eq!(pending.transaction.hash(), &tx0_hash, "Should be tx0");
2372                assert_eq!(
2373                    pending.promoted.len(),
2374                    1,
2375                    "Should have promoted 1 transaction"
2376                );
2377                assert_eq!(
2378                    pending.promoted[0].hash(),
2379                    &tx1_hash,
2380                    "Should have promoted tx1"
2381                );
2382            }
2383            _ => panic!("Transaction 0 should be pending, got: {added0:?}"),
2384        }
2385
2386        // Verify pool state after filling the gap
2387        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2388        assert_eq!(pending_count, 2, "Should have 2 pending transactions");
2389        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
2390
2391        // Verify both transactions are now pending
2392        let tx0_id = AA2dTransactionId::new(seq_id, 0);
2393        assert!(
2394            pool.by_id.get(&tx0_id).unwrap().is_pending(),
2395            "Transaction 0 should be pending"
2396        );
2397        assert!(
2398            pool.by_id.get(&tx1_id).unwrap().is_pending(),
2399            "Transaction 1 should be pending after promotion"
2400        );
2401
2402        // Verify tx0 (at on-chain nonce) is in independent set
2403        assert!(
2404            pool.independent_transactions.contains_key(&tx0_id.seq_id),
2405            "Transaction 0 should be in independent set (at on-chain nonce)"
2406        );
2407
2408        // Verify the independent transaction for this sequence is tx0, not tx1
2409        let independent_tx = pool.independent_transactions.get(&seq_id).unwrap();
2410        assert_eq!(
2411            independent_tx.transaction.hash(),
2412            &tx0_hash,
2413            "Independent transaction should be tx0, not tx1"
2414        );
2415
2416        pool.assert_invariants();
2417    }
2418
2419    #[test_case::test_case(U256::ZERO)]
2420    #[test_case::test_case(U256::random())]
2421    fn replace_pending_transaction(nonce_key: U256) {
2422        let mut pool = AA2dPool::default();
2423
2424        // Set up a sender with a tracked nonce key
2425        let sender = Address::random();
2426
2427        // Step 1: Insert initial pending transaction with lower gas price
2428        let tx_low = TxBuilder::aa(sender)
2429            .nonce_key(nonce_key)
2430            .max_priority_fee(1_000_000_000)
2431            .max_fee(2_000_000_000)
2432            .build();
2433        let valid_tx_low = wrap_valid_tx(tx_low, TransactionOrigin::Local);
2434        let tx_low_hash = *valid_tx_low.hash();
2435
2436        let result_low = pool.add_transaction(Arc::new(valid_tx_low), 0, TempoHardfork::T1);
2437
2438        // Should be pending (at on-chain nonce)
2439        assert!(
2440            result_low.is_ok(),
2441            "Initial transaction should be added successfully"
2442        );
2443        let added_low = result_low.unwrap();
2444        assert!(
2445            matches!(added_low, AddedTransaction::Pending(_)),
2446            "Initial transaction should be pending"
2447        );
2448
2449        // Verify initial state
2450        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2451        assert_eq!(pending_count, 1, "Should have 1 pending transaction");
2452        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
2453
2454        // Verify tx_low is in independent set
2455        let seq_id = AASequenceId::new(sender, nonce_key);
2456        let tx_id = AA2dTransactionId::new(seq_id, 0);
2457        assert!(
2458            pool.independent_transactions.contains_key(&tx_id.seq_id),
2459            "Initial transaction should be in independent set"
2460        );
2461
2462        // Verify the transaction in independent set is tx_low
2463        let independent_tx = pool.independent_transactions.get(&tx_id.seq_id).unwrap();
2464        assert_eq!(
2465            independent_tx.transaction.hash(),
2466            &tx_low_hash,
2467            "Independent set should contain tx_low"
2468        );
2469
2470        pool.assert_invariants();
2471
2472        // Step 2: Replace with higher gas price transaction
2473        // Price bump needs to be at least 10% higher (default price bump config)
2474        let tx_high = TxBuilder::aa(sender)
2475            .nonce_key(nonce_key)
2476            .max_priority_fee(1_200_000_000)
2477            .max_fee(2_400_000_000)
2478            .build();
2479        let valid_tx_high = wrap_valid_tx(tx_high, TransactionOrigin::Local);
2480        let tx_high_hash = *valid_tx_high.hash();
2481
2482        let result_high = pool.add_transaction(Arc::new(valid_tx_high), 0, TempoHardfork::T1);
2483
2484        // Should successfully replace
2485        assert!(
2486            result_high.is_ok(),
2487            "Replacement transaction should be added successfully"
2488        );
2489        let added_high = result_high.unwrap();
2490
2491        // Verify it's pending and replaced the old transaction
2492        match added_high {
2493            AddedTransaction::Pending(ref pending) => {
2494                assert_eq!(
2495                    pending.transaction.hash(),
2496                    &tx_high_hash,
2497                    "Should be tx_high"
2498                );
2499                assert!(
2500                    pending.replaced.is_some(),
2501                    "Should have replaced a transaction"
2502                );
2503                assert_eq!(
2504                    pending.replaced.as_ref().unwrap().hash(),
2505                    &tx_low_hash,
2506                    "Should have replaced tx_low"
2507                );
2508            }
2509            _ => panic!("Replacement transaction should be pending, got: {added_high:?}"),
2510        }
2511
2512        // Verify pool state - still 1 pending, 0 queued
2513        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2514        assert_eq!(
2515            pending_count, 1,
2516            "Should still have 1 pending transaction after replacement"
2517        );
2518        assert_eq!(queued_count, 0, "Should still have 0 queued transactions");
2519
2520        // Verify old transaction is no longer in the pool
2521        assert!(
2522            !pool.contains(&tx_low_hash),
2523            "Old transaction should be removed from pool"
2524        );
2525
2526        // Verify new transaction is in the pool
2527        assert!(
2528            pool.contains(&tx_high_hash),
2529            "New transaction should be in pool"
2530        );
2531
2532        // Verify independent set is updated with new transaction
2533        assert!(
2534            pool.independent_transactions.contains_key(&tx_id.seq_id),
2535            "Transaction ID should still be in independent set"
2536        );
2537
2538        let independent_tx_after = pool.independent_transactions.get(&tx_id.seq_id).unwrap();
2539        assert_eq!(
2540            independent_tx_after.transaction.hash(),
2541            &tx_high_hash,
2542            "Independent set should now contain tx_high (not tx_low)"
2543        );
2544
2545        // Verify the transaction in by_id is the new one
2546        let tx_in_pool = pool.by_id.get(&tx_id).unwrap();
2547        assert_eq!(
2548            tx_in_pool.inner.transaction.hash(),
2549            &tx_high_hash,
2550            "Transaction in by_id should be tx_high"
2551        );
2552        assert!(tx_in_pool.is_pending(), "Transaction should be pending");
2553
2554        pool.assert_invariants();
2555    }
2556
2557    #[test_case::test_case(U256::ZERO)]
2558    #[test_case::test_case(U256::random())]
2559    fn on_chain_nonce_update_with_gaps(nonce_key: U256) {
2560        let mut pool = AA2dPool::default();
2561
2562        // Set up a sender with a tracked nonce key
2563        let sender = Address::random();
2564
2565        // Insert transactions with nonces: 0, 1, 3, 4, 6
2566        // Expected initial state:
2567        // - 0, 1: pending (consecutive from on-chain nonce 0)
2568        // - 3, 4, 6: queued (gaps at nonce 2 and 5)
2569        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
2570        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
2571        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
2572        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
2573        let tx6 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(6).build();
2574
2575        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
2576        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
2577        let valid_tx3 = wrap_valid_tx(tx3, TransactionOrigin::Local);
2578        let valid_tx4 = wrap_valid_tx(tx4, TransactionOrigin::Local);
2579        let valid_tx6 = wrap_valid_tx(tx6, TransactionOrigin::Local);
2580
2581        let tx0_hash = *valid_tx0.hash();
2582        let tx1_hash = *valid_tx1.hash();
2583        let tx3_hash = *valid_tx3.hash();
2584        let tx4_hash = *valid_tx4.hash();
2585        let tx6_hash = *valid_tx6.hash();
2586
2587        // Add all transactions
2588        pool.add_transaction(Arc::new(valid_tx0), 0, TempoHardfork::T1)
2589            .unwrap();
2590        pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1)
2591            .unwrap();
2592        pool.add_transaction(Arc::new(valid_tx3), 0, TempoHardfork::T1)
2593            .unwrap();
2594        pool.add_transaction(Arc::new(valid_tx4), 0, TempoHardfork::T1)
2595            .unwrap();
2596        pool.add_transaction(Arc::new(valid_tx6), 0, TempoHardfork::T1)
2597            .unwrap();
2598
2599        // Verify initial state
2600        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2601        assert_eq!(
2602            pending_count, 2,
2603            "Should have 2 pending transactions (0, 1)"
2604        );
2605        assert_eq!(
2606            queued_count, 3,
2607            "Should have 3 queued transactions (3, 4, 6)"
2608        );
2609
2610        // Verify tx0 is in independent set
2611        let seq_id = AASequenceId::new(sender, nonce_key);
2612        let tx0_id = AA2dTransactionId::new(seq_id, 0);
2613        assert!(
2614            pool.independent_transactions.contains_key(&tx0_id.seq_id),
2615            "Transaction 0 should be in independent set"
2616        );
2617
2618        pool.assert_invariants();
2619
2620        // Step 1: Simulate mining block with nonces 0 and 1
2621        // New on-chain nonce becomes 2
2622        let mut on_chain_ids = HashMap::default();
2623        on_chain_ids.insert(seq_id, 2u64);
2624
2625        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
2626
2627        // Verify mined transactions
2628        assert_eq!(mined.len(), 2, "Should have mined 2 transactions (0, 1)");
2629        let mined_hashes: HashSet<_> = mined.iter().map(|tx| tx.hash()).collect();
2630        assert!(
2631            mined_hashes.contains(&&tx0_hash),
2632            "Transaction 0 should be mined"
2633        );
2634        assert!(
2635            mined_hashes.contains(&&tx1_hash),
2636            "Transaction 1 should be mined"
2637        );
2638
2639        // No transactions should be promoted (there's a gap at nonce 2)
2640        assert_eq!(
2641            promoted.len(),
2642            0,
2643            "No transactions should be promoted (gap at nonce 2)"
2644        );
2645
2646        // Verify pool state after mining
2647        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2648        assert_eq!(
2649            pending_count, 0,
2650            "Should have 0 pending transactions (gap at nonce 2)"
2651        );
2652        assert_eq!(
2653            queued_count, 3,
2654            "Should have 3 queued transactions (3, 4, 6)"
2655        );
2656
2657        // Verify mined transactions are removed
2658        assert!(!pool.contains(&tx0_hash), "Transaction 0 should be removed");
2659        assert!(!pool.contains(&tx1_hash), "Transaction 1 should be removed");
2660
2661        // Verify remaining transactions are still in pool
2662        assert!(pool.contains(&tx3_hash), "Transaction 3 should remain");
2663        assert!(pool.contains(&tx4_hash), "Transaction 4 should remain");
2664        assert!(pool.contains(&tx6_hash), "Transaction 6 should remain");
2665
2666        // Verify all remaining transactions are queued (not pending)
2667        let tx3_id = AA2dTransactionId::new(seq_id, 3);
2668        let tx4_id = AA2dTransactionId::new(seq_id, 4);
2669        let tx6_id = AA2dTransactionId::new(seq_id, 6);
2670
2671        assert!(
2672            !pool.by_id.get(&tx3_id).unwrap().is_pending(),
2673            "Transaction 3 should be queued (gap at nonce 2)"
2674        );
2675        assert!(
2676            !pool.by_id.get(&tx4_id).unwrap().is_pending(),
2677            "Transaction 4 should be queued"
2678        );
2679        assert!(
2680            !pool.by_id.get(&tx6_id).unwrap().is_pending(),
2681            "Transaction 6 should be queued"
2682        );
2683
2684        // Verify independent set is empty (no transaction at on-chain nonce)
2685        assert!(
2686            pool.independent_transactions.is_empty(),
2687            "Independent set should be empty (gap at on-chain nonce 2)"
2688        );
2689
2690        pool.assert_invariants();
2691
2692        // Step 2: Simulate mining block with nonce 2
2693        // New on-chain nonce becomes 3
2694        let mut on_chain_ids = HashMap::default();
2695        on_chain_ids.insert(seq_id, 3u64);
2696
2697        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
2698
2699        // No transactions should be mined (nonce 2 was never in pool)
2700        assert_eq!(
2701            mined.len(),
2702            0,
2703            "No transactions should be mined (nonce 2 was never in pool)"
2704        );
2705
2706        // Transactions 3 and 4 should be promoted
2707        assert_eq!(promoted.len(), 2, "Transactions 3 and 4 should be promoted");
2708        let promoted_hashes: HashSet<_> = promoted.iter().map(|tx| tx.hash()).collect();
2709        assert!(
2710            promoted_hashes.contains(&&tx3_hash),
2711            "Transaction 3 should be promoted"
2712        );
2713        assert!(
2714            promoted_hashes.contains(&&tx4_hash),
2715            "Transaction 4 should be promoted"
2716        );
2717
2718        // Verify pool state after second update
2719        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2720        assert_eq!(
2721            pending_count, 2,
2722            "Should have 2 pending transactions (3, 4)"
2723        );
2724        assert_eq!(queued_count, 1, "Should have 1 queued transaction (6)");
2725
2726        // Verify transactions 3 and 4 are now pending
2727        assert!(
2728            pool.by_id.get(&tx3_id).unwrap().is_pending(),
2729            "Transaction 3 should be pending"
2730        );
2731        assert!(
2732            pool.by_id.get(&tx4_id).unwrap().is_pending(),
2733            "Transaction 4 should be pending"
2734        );
2735
2736        // Verify transaction 6 is still queued
2737        assert!(
2738            !pool.by_id.get(&tx6_id).unwrap().is_pending(),
2739            "Transaction 6 should still be queued (gap at nonce 5)"
2740        );
2741
2742        // Verify transaction 3 is the independent transaction (at on-chain nonce)
2743        assert!(
2744            pool.independent_transactions.contains_key(&tx3_id.seq_id),
2745            "Transaction 3 should be in independent set (at on-chain nonce 3)"
2746        );
2747
2748        // Verify the independent transaction is tx3 specifically, not tx4 or tx6
2749        let independent_tx = pool.independent_transactions.get(&seq_id).unwrap();
2750        assert_eq!(
2751            independent_tx.transaction.hash(),
2752            &tx3_hash,
2753            "Independent transaction should be tx3 (nonce 3), not tx4 or tx6"
2754        );
2755
2756        pool.assert_invariants();
2757    }
2758
2759    #[test_case::test_case(U256::ZERO)]
2760    #[test_case::test_case(U256::random())]
2761    fn reject_outdated_transaction(nonce_key: U256) {
2762        let mut pool = AA2dPool::default();
2763
2764        // Set up a sender with a tracked nonce key
2765        let sender = Address::random();
2766
2767        // Create a transaction with nonce 3 (outdated)
2768        let tx = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
2769        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
2770
2771        // Try to insert it and specify the on-chain nonce 5, making it outdated
2772        let result = pool.add_transaction(Arc::new(valid_tx), 5, TempoHardfork::T1);
2773
2774        // Should fail with nonce error
2775        assert!(result.is_err(), "Should reject outdated transaction");
2776
2777        let err = result.unwrap_err();
2778        assert!(
2779            matches!(
2780                err.kind,
2781                PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus(
2782                    InvalidTransactionError::NonceNotConsistent { tx: 3, state: 5 }
2783                ))
2784            ),
2785            "Should fail with NonceNotConsistent error, got: {:?}",
2786            err.kind
2787        );
2788
2789        // Pool should remain empty
2790        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2791        assert_eq!(pending_count, 0, "Pool should be empty");
2792        assert_eq!(queued_count, 0, "Pool should be empty");
2793
2794        pool.assert_invariants();
2795    }
2796
2797    #[test_case::test_case(U256::ZERO)]
2798    #[test_case::test_case(U256::random())]
2799    fn replace_with_insufficient_price_bump(nonce_key: U256) {
2800        let mut pool = AA2dPool::default();
2801
2802        // Set up a sender
2803        let sender = Address::random();
2804
2805        // Insert initial transaction
2806        let tx_low = TxBuilder::aa(sender)
2807            .nonce_key(nonce_key)
2808            .max_priority_fee(1_000_000_000)
2809            .max_fee(2_000_000_000)
2810            .build();
2811        let valid_tx_low = wrap_valid_tx(tx_low, TransactionOrigin::Local);
2812
2813        pool.add_transaction(Arc::new(valid_tx_low), 0, TempoHardfork::T1)
2814            .unwrap();
2815
2816        // Try to replace with only 5% price bump (default requires 10%)
2817        let tx_insufficient = TxBuilder::aa(sender)
2818            .nonce_key(nonce_key)
2819            .max_priority_fee(1_050_000_000)
2820            .max_fee(2_100_000_000)
2821            .build();
2822        let valid_tx_insufficient = wrap_valid_tx(tx_insufficient, TransactionOrigin::Local);
2823
2824        let result = pool.add_transaction(Arc::new(valid_tx_insufficient), 0, TempoHardfork::T1);
2825
2826        // Should fail with ReplacementUnderpriced
2827        assert!(
2828            result.is_err(),
2829            "Should reject replacement with insufficient price bump"
2830        );
2831        let err = result.unwrap_err();
2832        assert!(
2833            matches!(err.kind, PoolErrorKind::ReplacementUnderpriced),
2834            "Should fail with ReplacementUnderpriced, got: {:?}",
2835            err.kind
2836        );
2837
2838        pool.assert_invariants();
2839    }
2840
2841    #[test_case::test_case(U256::ZERO)]
2842    #[test_case::test_case(U256::random())]
2843    fn fill_gap_in_middle(nonce_key: U256) {
2844        let mut pool = AA2dPool::default();
2845
2846        let sender = Address::random();
2847
2848        // Insert transactions: 0, 1, 3, 4 (gap at 2)
2849        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
2850        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
2851        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
2852        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
2853
2854        pool.add_transaction(
2855            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
2856            0,
2857            TempoHardfork::T1,
2858        )
2859        .unwrap();
2860        pool.add_transaction(
2861            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
2862            0,
2863            TempoHardfork::T1,
2864        )
2865        .unwrap();
2866        pool.add_transaction(
2867            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
2868            0,
2869            TempoHardfork::T1,
2870        )
2871        .unwrap();
2872        pool.add_transaction(
2873            Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)),
2874            0,
2875            TempoHardfork::T1,
2876        )
2877        .unwrap();
2878
2879        // Verify initial state: 0, 1 pending | 3, 4 queued
2880        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2881        assert_eq!(pending_count, 2, "Should have 2 pending (0, 1)");
2882        assert_eq!(queued_count, 2, "Should have 2 queued (3, 4)");
2883
2884        // Fill the gap with nonce 2
2885        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
2886        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
2887
2888        let result = pool.add_transaction(Arc::new(valid_tx2), 0, TempoHardfork::T1);
2889        assert!(result.is_ok(), "Should successfully add tx2");
2890
2891        // Verify tx3 and tx4 were promoted
2892        match result.unwrap() {
2893            AddedTransaction::Pending(ref pending) => {
2894                assert_eq!(pending.promoted.len(), 2, "Should promote tx3 and tx4");
2895            }
2896            _ => panic!("tx2 should be added as pending"),
2897        }
2898
2899        // Verify all transactions are now pending
2900        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2901        assert_eq!(pending_count, 5, "All 5 transactions should be pending");
2902        assert_eq!(queued_count, 0, "No transactions should be queued");
2903
2904        // Verify tx0 is in independent set
2905        let seq_id = AASequenceId::new(sender, nonce_key);
2906        let tx0_id = AA2dTransactionId::new(seq_id, 0);
2907        assert!(
2908            pool.independent_transactions.contains_key(&tx0_id.seq_id),
2909            "tx0 should be in independent set"
2910        );
2911
2912        pool.assert_invariants();
2913    }
2914
2915    #[test_case::test_case(U256::ZERO)]
2916    #[test_case::test_case(U256::random())]
2917    fn remove_pending_transaction(nonce_key: U256) {
2918        let mut pool = AA2dPool::default();
2919
2920        let sender = Address::random();
2921
2922        // Insert consecutive transactions: 0, 1, 2
2923        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
2924        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
2925        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
2926
2927        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
2928        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
2929        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
2930
2931        let tx1_hash = *valid_tx1.hash();
2932
2933        pool.add_transaction(Arc::new(valid_tx0), 0, TempoHardfork::T1)
2934            .unwrap();
2935        pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1)
2936            .unwrap();
2937        pool.add_transaction(Arc::new(valid_tx2), 0, TempoHardfork::T1)
2938            .unwrap();
2939
2940        // All should be pending
2941        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2942        assert_eq!(pending_count, 3, "All 3 should be pending");
2943        assert_eq!(queued_count, 0, "None should be queued");
2944
2945        let seq_id = AASequenceId::new(sender, nonce_key);
2946        let tx1_id = AA2dTransactionId::new(seq_id, 1);
2947        let tx2_id = AA2dTransactionId::new(seq_id, 2);
2948
2949        // Verify tx2 is pending before removal
2950        assert!(
2951            pool.by_id.get(&tx2_id).unwrap().is_pending(),
2952            "tx2 should be pending before removal"
2953        );
2954
2955        // Remove tx1 (creates a gap)
2956        let removed = pool.remove_transactions([&tx1_hash].into_iter());
2957        assert_eq!(removed.len(), 1, "Should remove tx1");
2958
2959        // Verify tx1 is removed from pool
2960        assert!(!pool.by_id.contains_key(&tx1_id), "tx1 should be removed");
2961        assert!(!pool.contains(&tx1_hash), "tx1 should be removed");
2962
2963        // Verify tx0 and tx2 remain
2964        assert_eq!(pool.by_id.len(), 2, "Should have 2 transactions left");
2965
2966        // Verify tx2 is now demoted to queued since tx1 removal creates a gap
2967        assert!(
2968            !pool.by_id.get(&tx2_id).unwrap().is_pending(),
2969            "tx2 should be demoted to queued after tx1 removal creates a gap"
2970        );
2971
2972        // Verify counts: tx0 is pending, tx2 is queued
2973        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2974        assert_eq!(pending_count, 1, "Only tx0 should be pending");
2975        assert_eq!(queued_count, 1, "tx2 should be queued");
2976
2977        pool.assert_invariants();
2978    }
2979
2980    #[test_case::test_case(U256::ZERO, U256::random())]
2981    #[test_case::test_case(U256::random(), U256::ZERO)]
2982    #[test_case::test_case(U256::random(), U256::random())]
2983    fn multiple_senders_independent_set(nonce_key_a: U256, nonce_key_b: U256) {
2984        let mut pool = AA2dPool::default();
2985
2986        // Set up two senders with different nonce keys
2987        let sender_a = Address::random();
2988        let sender_b = Address::random();
2989
2990        // Insert transactions for both senders
2991        // Sender A: [0, 1]
2992        let tx_a0 = TxBuilder::aa(sender_a).nonce_key(nonce_key_a).build();
2993        let tx_a1 = TxBuilder::aa(sender_a)
2994            .nonce_key(nonce_key_a)
2995            .nonce(1)
2996            .build();
2997
2998        // Sender B: [0, 1]
2999        let tx_b0 = TxBuilder::aa(sender_b).nonce_key(nonce_key_b).build();
3000        let tx_b1 = TxBuilder::aa(sender_b)
3001            .nonce_key(nonce_key_b)
3002            .nonce(1)
3003            .build();
3004
3005        let valid_tx_a0 = wrap_valid_tx(tx_a0, TransactionOrigin::Local);
3006        let valid_tx_a1 = wrap_valid_tx(tx_a1, TransactionOrigin::Local);
3007        let valid_tx_b0 = wrap_valid_tx(tx_b0, TransactionOrigin::Local);
3008        let valid_tx_b1 = wrap_valid_tx(tx_b1, TransactionOrigin::Local);
3009
3010        let tx_a0_hash = *valid_tx_a0.hash();
3011
3012        pool.add_transaction(Arc::new(valid_tx_a0), 0, TempoHardfork::T1)
3013            .unwrap();
3014        pool.add_transaction(Arc::new(valid_tx_a1), 0, TempoHardfork::T1)
3015            .unwrap();
3016        pool.add_transaction(Arc::new(valid_tx_b0), 0, TempoHardfork::T1)
3017            .unwrap();
3018        pool.add_transaction(Arc::new(valid_tx_b1), 0, TempoHardfork::T1)
3019            .unwrap();
3020
3021        // Both senders' tx0 should be in independent set
3022        let sender_a_id = AASequenceId::new(sender_a, nonce_key_a);
3023        let sender_b_id = AASequenceId::new(sender_b, nonce_key_b);
3024        let tx_a0_id = AA2dTransactionId::new(sender_a_id, 0);
3025        let tx_b0_id = AA2dTransactionId::new(sender_b_id, 0);
3026
3027        assert_eq!(
3028            pool.independent_transactions.len(),
3029            2,
3030            "Should have 2 independent transactions"
3031        );
3032        assert!(
3033            pool.independent_transactions.contains_key(&tx_a0_id.seq_id),
3034            "Sender A's tx0 should be independent"
3035        );
3036        assert!(
3037            pool.independent_transactions.contains_key(&tx_b0_id.seq_id),
3038            "Sender B's tx0 should be independent"
3039        );
3040
3041        // All 4 transactions should be pending
3042        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3043        assert_eq!(pending_count, 4, "All 4 transactions should be pending");
3044        assert_eq!(queued_count, 0, "No transactions should be queued");
3045
3046        // Simulate mining sender A's tx0
3047        let mut on_chain_ids = HashMap::default();
3048        on_chain_ids.insert(sender_a_id, 1u64);
3049
3050        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
3051
3052        // Only sender A's tx0 should be mined
3053        assert_eq!(mined.len(), 1, "Only sender A's tx0 should be mined");
3054        assert_eq!(mined[0].hash(), &tx_a0_hash, "Should mine tx_a0");
3055
3056        // No transactions should be promoted (tx_a1 was already pending)
3057        assert_eq!(
3058            promoted.len(),
3059            0,
3060            "No transactions should be promoted (tx_a1 was already pending)"
3061        );
3062
3063        // Verify independent set now has A's tx1 and B's tx0
3064        let tx_a1_id = AA2dTransactionId::new(sender_a_id, 1);
3065        assert_eq!(
3066            pool.independent_transactions.len(),
3067            2,
3068            "Should still have 2 independent transactions"
3069        );
3070        assert!(
3071            pool.independent_transactions.contains_key(&tx_a1_id.seq_id),
3072            "Sender A's tx1 should now be independent"
3073        );
3074        assert!(
3075            pool.independent_transactions.contains_key(&tx_b0_id.seq_id),
3076            "Sender B's tx0 should still be independent"
3077        );
3078
3079        pool.assert_invariants();
3080    }
3081
3082    #[test_case::test_case(U256::ZERO)]
3083    #[test_case::test_case(U256::random())]
3084    fn concurrent_replacements_same_nonce(nonce_key: U256) {
3085        let mut pool = AA2dPool::default();
3086        let sender = Address::random();
3087        let seq_id = AASequenceId {
3088            address: sender,
3089            nonce_key,
3090        };
3091
3092        // Insert initial transaction at nonce 0 with gas prices 1_000_000_000, 2_000_000_000
3093        let tx0 = TxBuilder::aa(sender)
3094            .nonce_key(nonce_key)
3095            .max_priority_fee(1_000_000_000)
3096            .max_fee(2_000_000_000)
3097            .build();
3098        let tx0_hash = *tx0.hash();
3099        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
3100        let result = pool.add_transaction(Arc::new(valid_tx0), 0, TempoHardfork::T1);
3101        assert!(result.is_ok());
3102        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3103        assert_eq!(pending_count + queued_count, 1);
3104
3105        // Try to replace with slightly higher gas (1_050_000_000, 2_100_000_000 = ~5% bump) - should fail (< 10% bump)
3106        let tx0_replacement1 = TxBuilder::aa(sender)
3107            .nonce_key(nonce_key)
3108            .max_priority_fee(1_050_000_000)
3109            .max_fee(2_100_000_000)
3110            .build();
3111        let valid_tx1 = wrap_valid_tx(tx0_replacement1, TransactionOrigin::Local);
3112        let result = pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1);
3113        assert!(result.is_err(), "Should reject insufficient price bump");
3114        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3115        assert_eq!(pending_count + queued_count, 1);
3116        assert!(
3117            pool.contains(&tx0_hash),
3118            "Original tx should still be present"
3119        );
3120
3121        // Replace with sufficient bump (1_100_000_000, 2_200_000_000 = 10% bump)
3122        let tx0_replacement2 = TxBuilder::aa(sender)
3123            .nonce_key(nonce_key)
3124            .max_priority_fee(1_100_000_000)
3125            .max_fee(2_200_000_000)
3126            .build();
3127        let tx0_replacement2_hash = *tx0_replacement2.hash();
3128        let valid_tx2 = wrap_valid_tx(tx0_replacement2, TransactionOrigin::Local);
3129        let result = pool.add_transaction(Arc::new(valid_tx2), 0, TempoHardfork::T1);
3130        assert!(result.is_ok(), "Should accept 10% price bump");
3131        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3132        assert_eq!(pending_count + queued_count, 1, "Pool size should remain 1");
3133        assert!(!pool.contains(&tx0_hash), "Old tx should be removed");
3134        assert!(
3135            pool.contains(&tx0_replacement2_hash),
3136            "New tx should be present"
3137        );
3138
3139        // Try to replace with even higher gas (1_500_000_000, 3_000_000_000 = ~36% bump over original)
3140        let tx0_replacement3 = TxBuilder::aa(sender)
3141            .nonce_key(nonce_key)
3142            .max_priority_fee(1_500_000_000)
3143            .max_fee(3_000_000_000)
3144            .build();
3145        let tx0_replacement3_hash = *tx0_replacement3.hash();
3146        let valid_tx3 = wrap_valid_tx(tx0_replacement3, TransactionOrigin::Local);
3147        let result = pool.add_transaction(Arc::new(valid_tx3), 0, TempoHardfork::T1);
3148        assert!(result.is_ok(), "Should accept higher price bump");
3149        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3150        assert_eq!(pending_count + queued_count, 1);
3151        assert!(
3152            !pool.contains(&tx0_replacement2_hash),
3153            "Previous tx should be removed"
3154        );
3155        assert!(
3156            pool.contains(&tx0_replacement3_hash),
3157            "Highest priority tx should win"
3158        );
3159
3160        // Verify independent set has the final replacement
3161        let tx0_id = AA2dTransactionId::new(seq_id, 0);
3162        assert!(pool.independent_transactions.contains_key(&tx0_id.seq_id));
3163
3164        pool.assert_invariants();
3165    }
3166
3167    #[test_case::test_case(U256::ZERO)]
3168    #[test_case::test_case(U256::random())]
3169    fn long_gap_chain(nonce_key: U256) {
3170        let mut pool = AA2dPool::default();
3171        let sender = Address::random();
3172        let seq_id = AASequenceId {
3173            address: sender,
3174            nonce_key,
3175        };
3176
3177        // Insert transactions with large gaps: [0, 5, 10, 15]
3178        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
3179        let tx5 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(5).build();
3180        let tx10 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(10).build();
3181        let tx15 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(15).build();
3182
3183        pool.add_transaction(
3184            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
3185            0,
3186            TempoHardfork::T1,
3187        )
3188        .unwrap();
3189        pool.add_transaction(
3190            Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)),
3191            0,
3192            TempoHardfork::T1,
3193        )
3194        .unwrap();
3195        pool.add_transaction(
3196            Arc::new(wrap_valid_tx(tx10, TransactionOrigin::Local)),
3197            0,
3198            TempoHardfork::T1,
3199        )
3200        .unwrap();
3201        pool.add_transaction(
3202            Arc::new(wrap_valid_tx(tx15, TransactionOrigin::Local)),
3203            0,
3204            TempoHardfork::T1,
3205        )
3206        .unwrap();
3207
3208        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3209        assert_eq!(pending_count + queued_count, 4);
3210
3211        // Only tx0 should be pending, rest should be queued
3212        let tx0_id = AA2dTransactionId::new(seq_id, 0);
3213        assert!(pool.by_id.get(&tx0_id).unwrap().is_pending());
3214        assert!(
3215            !pool
3216                .by_id
3217                .get(&AA2dTransactionId::new(seq_id, 5))
3218                .unwrap()
3219                .is_pending()
3220        );
3221        assert!(
3222            !pool
3223                .by_id
3224                .get(&AA2dTransactionId::new(seq_id, 10))
3225                .unwrap()
3226                .is_pending()
3227        );
3228        assert!(
3229            !pool
3230                .by_id
3231                .get(&AA2dTransactionId::new(seq_id, 15))
3232                .unwrap()
3233                .is_pending()
3234        );
3235        assert_eq!(pool.independent_transactions.len(), 1);
3236
3237        // Fill gap [1,2,3,4]
3238        for nonce in 1..=4 {
3239            let tx = TxBuilder::aa(sender)
3240                .nonce_key(nonce_key)
3241                .nonce(nonce)
3242                .build();
3243            pool.add_transaction(
3244                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3245                0,
3246                TempoHardfork::T1,
3247            )
3248            .unwrap();
3249        }
3250
3251        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3252        assert_eq!(pending_count + queued_count, 8);
3253
3254        // Now [0,1,2,3,4,5] should be pending
3255        for nonce in 0..=5 {
3256            let id = AA2dTransactionId::new(seq_id, nonce);
3257            assert!(
3258                pool.by_id.get(&id).unwrap().is_pending(),
3259                "Nonce {nonce} should be pending"
3260            );
3261        }
3262        // [10, 15] should still be queued
3263        assert!(
3264            !pool
3265                .by_id
3266                .get(&AA2dTransactionId::new(seq_id, 10))
3267                .unwrap()
3268                .is_pending()
3269        );
3270        assert!(
3271            !pool
3272                .by_id
3273                .get(&AA2dTransactionId::new(seq_id, 15))
3274                .unwrap()
3275                .is_pending()
3276        );
3277
3278        // Fill gap [6,7,8,9]
3279        for nonce in 6..=9 {
3280            let tx = TxBuilder::aa(sender)
3281                .nonce_key(nonce_key)
3282                .nonce(nonce)
3283                .build();
3284            pool.add_transaction(
3285                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3286                0,
3287                TempoHardfork::T1,
3288            )
3289            .unwrap();
3290        }
3291
3292        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3293        assert_eq!(pending_count + queued_count, 12);
3294
3295        // Now [0..=10] should be pending
3296        for nonce in 0..=10 {
3297            let id = AA2dTransactionId::new(seq_id, nonce);
3298            assert!(
3299                pool.by_id.get(&id).unwrap().is_pending(),
3300                "Nonce {nonce} should be pending"
3301            );
3302        }
3303        // Only [15] should be queued
3304        assert!(
3305            !pool
3306                .by_id
3307                .get(&AA2dTransactionId::new(seq_id, 15))
3308                .unwrap()
3309                .is_pending()
3310        );
3311
3312        // Fill final gap [11,12,13,14]
3313        for nonce in 11..=14 {
3314            let tx = TxBuilder::aa(sender)
3315                .nonce_key(nonce_key)
3316                .nonce(nonce)
3317                .build();
3318            pool.add_transaction(
3319                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3320                0,
3321                TempoHardfork::T1,
3322            )
3323            .unwrap();
3324        }
3325
3326        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3327        assert_eq!(pending_count + queued_count, 16);
3328
3329        // All should be pending now
3330        for nonce in 0..=15 {
3331            let id = AA2dTransactionId::new(seq_id, nonce);
3332            assert!(
3333                pool.by_id.get(&id).unwrap().is_pending(),
3334                "Nonce {nonce} should be pending"
3335            );
3336        }
3337
3338        pool.assert_invariants();
3339    }
3340
3341    #[test_case::test_case(U256::ZERO)]
3342    #[test_case::test_case(U256::random())]
3343    fn remove_from_middle_of_chain(nonce_key: U256) {
3344        let mut pool = AA2dPool::default();
3345        let sender = Address::random();
3346        let seq_id = AASequenceId {
3347            address: sender,
3348            nonce_key,
3349        };
3350
3351        // Insert continuous sequence [0,1,2,3,4]
3352        for nonce in 0..=4 {
3353            let tx = TxBuilder::aa(sender)
3354                .nonce_key(nonce_key)
3355                .nonce(nonce)
3356                .build();
3357            pool.add_transaction(
3358                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3359                0,
3360                TempoHardfork::T1,
3361            )
3362            .unwrap();
3363        }
3364
3365        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3366        assert_eq!(pending_count + queued_count, 5);
3367
3368        // All should be pending
3369        for nonce in 0..=4 {
3370            assert!(
3371                pool.by_id
3372                    .get(&AA2dTransactionId::new(seq_id, nonce))
3373                    .unwrap()
3374                    .is_pending()
3375            );
3376        }
3377
3378        // Remove nonce 2 from the middle
3379        let tx2_id = AA2dTransactionId::new(seq_id, 2);
3380        let tx2_hash = *pool.by_id.get(&tx2_id).unwrap().inner.transaction.hash();
3381        let removed = pool.remove_transactions([&tx2_hash].into_iter());
3382        assert_eq!(removed.len(), 1, "Should remove transaction");
3383
3384        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3385        assert_eq!(pending_count + queued_count, 4);
3386
3387        // Transaction 2 should be gone
3388        assert!(!pool.by_id.contains_key(&tx2_id));
3389
3390        // Note: Current implementation doesn't automatically re-scan after removal
3391        // So we verify that the removal succeeded but don't expect automatic gap detection
3392        // Transactions [0,1,3,4] remain in their current state
3393
3394        pool.assert_invariants();
3395    }
3396
3397    #[test_case::test_case(U256::ZERO)]
3398    #[test_case::test_case(U256::random())]
3399    fn independent_set_after_multiple_promotions(nonce_key: U256) {
3400        let mut pool = AA2dPool::default();
3401        let sender = Address::random();
3402        let seq_id = AASequenceId {
3403            address: sender,
3404            nonce_key,
3405        };
3406
3407        // Start with gaps: insert [0, 2, 4]
3408        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
3409        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
3410        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
3411
3412        pool.add_transaction(
3413            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
3414            0,
3415            TempoHardfork::T1,
3416        )
3417        .unwrap();
3418        pool.add_transaction(
3419            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
3420            0,
3421            TempoHardfork::T1,
3422        )
3423        .unwrap();
3424        pool.add_transaction(
3425            Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)),
3426            0,
3427            TempoHardfork::T1,
3428        )
3429        .unwrap();
3430
3431        // Only tx0 should be in independent set
3432        assert_eq!(pool.independent_transactions.len(), 1);
3433        assert!(pool.independent_transactions.contains_key(&seq_id));
3434
3435        // Verify initial state: tx0 pending, tx2 and tx4 queued
3436        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3437        assert_eq!(pending_count, 1);
3438        assert_eq!(queued_count, 2);
3439
3440        // Fill first gap: insert [1]
3441        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
3442        pool.add_transaction(
3443            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3444            0,
3445            TempoHardfork::T1,
3446        )
3447        .unwrap();
3448
3449        // Now [0, 1, 2] should be pending, tx4 still queued
3450        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3451        assert_eq!(pending_count, 3);
3452        assert_eq!(queued_count, 1);
3453
3454        // Still only tx0 in independent set
3455        assert_eq!(pool.independent_transactions.len(), 1);
3456        assert!(pool.independent_transactions.contains_key(&seq_id));
3457
3458        // Fill second gap: insert [3]
3459        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
3460        pool.add_transaction(
3461            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
3462            0,
3463            TempoHardfork::T1,
3464        )
3465        .unwrap();
3466
3467        // Now all [0,1,2,3,4] should be pending
3468        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3469        assert_eq!(pending_count, 5);
3470        assert_eq!(queued_count, 0);
3471
3472        // Simulate mining [0,1]
3473        let mut on_chain_ids = HashMap::default();
3474        on_chain_ids.insert(seq_id, 2u64);
3475        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
3476
3477        // Should have mined [0,1], no promotions (already pending)
3478        assert_eq!(mined.len(), 2);
3479        assert_eq!(promoted.len(), 0);
3480
3481        // Now tx2 should be in independent set
3482        assert_eq!(pool.independent_transactions.len(), 1);
3483        assert!(pool.independent_transactions.contains_key(&seq_id));
3484
3485        // Verify [2,3,4] remain in pool
3486        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3487        assert_eq!(pending_count + queued_count, 3);
3488
3489        pool.assert_invariants();
3490    }
3491
3492    #[test]
3493    fn stress_test_many_senders() {
3494        let mut pool = AA2dPool::default();
3495        const NUM_SENDERS: usize = 100;
3496        const TXS_PER_SENDER: u64 = 5;
3497
3498        // Create 100 senders, each with 5 transactions
3499        let mut senders = Vec::new();
3500        for i in 0..NUM_SENDERS {
3501            let sender = Address::from_word(B256::from(U256::from(i)));
3502            let nonce_key = U256::from(i);
3503            senders.push((sender, nonce_key));
3504
3505            // Insert transactions [0,1,2,3,4] for each sender
3506            for nonce in 0..TXS_PER_SENDER {
3507                let tx = TxBuilder::aa(sender)
3508                    .nonce_key(nonce_key)
3509                    .nonce(nonce)
3510                    .build();
3511                pool.add_transaction(
3512                    Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3513                    0,
3514                    TempoHardfork::T1,
3515                )
3516                .unwrap();
3517            }
3518        }
3519
3520        // Verify pool size
3521        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3522        assert_eq!(
3523            pending_count + queued_count,
3524            NUM_SENDERS * TXS_PER_SENDER as usize
3525        );
3526
3527        // Each sender should have all transactions pending
3528        for (sender, nonce_key) in &senders {
3529            let seq_id = AASequenceId {
3530                address: *sender,
3531                nonce_key: *nonce_key,
3532            };
3533            for nonce in 0..TXS_PER_SENDER {
3534                let id = AA2dTransactionId::new(seq_id, nonce);
3535                assert!(pool.by_id.get(&id).unwrap().is_pending());
3536            }
3537        }
3538
3539        // Independent set should have exactly NUM_SENDERS transactions (one per sender at nonce 0)
3540        assert_eq!(pool.independent_transactions.len(), NUM_SENDERS);
3541        for (sender, nonce_key) in &senders {
3542            let seq_id = AASequenceId {
3543                address: *sender,
3544                nonce_key: *nonce_key,
3545            };
3546            let tx0_id = AA2dTransactionId::new(seq_id, 0);
3547            assert!(
3548                pool.independent_transactions.contains_key(&tx0_id.seq_id),
3549                "Sender {sender:?} should have tx0 in independent set"
3550            );
3551        }
3552
3553        // Simulate mining first transaction for each sender
3554        let mut on_chain_ids = HashMap::default();
3555        for (sender, nonce_key) in &senders {
3556            let seq_id = AASequenceId {
3557                address: *sender,
3558                nonce_key: *nonce_key,
3559            };
3560            on_chain_ids.insert(seq_id, 1u64);
3561        }
3562
3563        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
3564
3565        // Should have mined NUM_SENDERS transactions
3566        assert_eq!(mined.len(), NUM_SENDERS);
3567        // No promotions - transactions [1,2,3,4] were already pending
3568        assert_eq!(promoted.len(), 0);
3569
3570        // Pool size should be reduced
3571        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3572        assert_eq!(
3573            pending_count + queued_count,
3574            NUM_SENDERS * (TXS_PER_SENDER - 1) as usize
3575        );
3576
3577        // Independent set should still have NUM_SENDERS transactions (now at nonce 1)
3578        assert_eq!(pool.independent_transactions.len(), NUM_SENDERS);
3579        for (sender, nonce_key) in &senders {
3580            let seq_id = AASequenceId {
3581                address: *sender,
3582                nonce_key: *nonce_key,
3583            };
3584            let tx1_id = AA2dTransactionId::new(seq_id, 1);
3585            assert!(
3586                pool.independent_transactions.contains_key(&tx1_id.seq_id),
3587                "Sender {sender:?} should have tx1 in independent set"
3588            );
3589        }
3590
3591        pool.assert_invariants();
3592    }
3593
3594    #[test_case::test_case(U256::ZERO)]
3595    #[test_case::test_case(U256::random())]
3596    fn on_chain_nonce_update_to_queued_tx_with_gaps(nonce_key: U256) {
3597        let mut pool = AA2dPool::default();
3598        let sender = Address::random();
3599        let seq_id = AASequenceId {
3600            address: sender,
3601            nonce_key,
3602        };
3603
3604        // Start with gaps: insert [0, 3, 5]
3605        // This creates: tx0 (pending), tx3 (queued), tx5 (queued)
3606        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
3607        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
3608        let tx5 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(5).build();
3609
3610        pool.add_transaction(
3611            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
3612            0,
3613            TempoHardfork::T1,
3614        )
3615        .unwrap();
3616        pool.add_transaction(
3617            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
3618            0,
3619            TempoHardfork::T1,
3620        )
3621        .unwrap();
3622        pool.add_transaction(
3623            Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)),
3624            0,
3625            TempoHardfork::T1,
3626        )
3627        .unwrap();
3628
3629        // Only tx0 should be in independent set
3630        assert_eq!(pool.independent_transactions.len(), 1);
3631        assert!(pool.independent_transactions.contains_key(&seq_id));
3632
3633        // Verify initial state: tx0 pending, tx3 and tx5 queued
3634        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3635        assert_eq!(pending_count, 1, "Only tx0 should be pending");
3636        assert_eq!(queued_count, 2, "tx3 and tx5 should be queued");
3637
3638        // Fill gaps to get [0, 1, 2, 3, 5]
3639        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
3640        pool.add_transaction(
3641            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3642            0,
3643            TempoHardfork::T1,
3644        )
3645        .unwrap();
3646
3647        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
3648        pool.add_transaction(
3649            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
3650            0,
3651            TempoHardfork::T1,
3652        )
3653        .unwrap();
3654
3655        // Now [0,1,2,3] should be pending, tx5 still queued
3656        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
3657        assert_eq!(pending_count, 4, "Transactions [0,1,2,3] should be pending");
3658        assert_eq!(queued_count, 1, "tx5 should still be queued");
3659
3660        // Still only tx0 in independent set (at on-chain nonce 0)
3661        assert_eq!(pool.independent_transactions.len(), 1);
3662        assert!(pool.independent_transactions.contains_key(&seq_id));
3663
3664        let mut on_chain_ids = HashMap::default();
3665        on_chain_ids.insert(seq_id, 3u64);
3666        let (_promoted, mined) = pool.on_nonce_changes(on_chain_ids);
3667
3668        // Should have mined [0,1,2]
3669        assert_eq!(mined.len(), 3, "Should mine transactions [0,1,2]");
3670
3671        // tx3 was already pending, so no promotions expected
3672        // After mining, tx3 should be in independent set
3673        assert_eq!(
3674            pool.independent_transactions.len(),
3675            1,
3676            "Should have one independent transaction"
3677        );
3678        let key = AA2dTransactionId::new(seq_id, 3);
3679        assert!(
3680            pool.independent_transactions.contains_key(&key.seq_id),
3681            "tx3 should be in independent set"
3682        );
3683
3684        // Verify remaining pool state
3685        let (_pending_count, _queued_count) = pool.pending_and_queued_txn_count();
3686        // Should have tx3 (pending at on-chain nonce) and tx5 (queued due to gap at 4)
3687
3688        pool.assert_invariants();
3689
3690        // Now insert tx4 to fill the gap between tx3 and tx5
3691        // This is where the original test failure occurred
3692        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
3693        pool.add_transaction(
3694            Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)),
3695            3,
3696            TempoHardfork::T1,
3697        )
3698        .unwrap();
3699
3700        // After inserting tx4, we should have [3, 4, 5] all in the pool
3701        let (_pending_count_after, _queued_count_after) = pool.pending_and_queued_txn_count();
3702        pool.assert_invariants();
3703    }
3704
3705    #[test]
3706    fn append_pooled_transaction_elements_respects_limit() {
3707        let mut pool = AA2dPool::default();
3708        let sender = Address::random();
3709        let nonce_key = U256::from(1);
3710
3711        // Add 3 transactions with consecutive nonces
3712        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
3713        let tx0_hash = *tx0.hash();
3714        let tx0_len = tx0.encoded_length();
3715        pool.add_transaction(
3716            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
3717            0,
3718            TempoHardfork::T1,
3719        )
3720        .unwrap();
3721
3722        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
3723        let tx1_hash = *tx1.hash();
3724        let tx1_len = tx1.encoded_length();
3725        pool.add_transaction(
3726            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3727            0,
3728            TempoHardfork::T1,
3729        )
3730        .unwrap();
3731
3732        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
3733        let tx2_hash = *tx2.hash();
3734        let tx2_len = tx2.encoded_length();
3735        pool.add_transaction(
3736            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
3737            0,
3738            TempoHardfork::T1,
3739        )
3740        .unwrap();
3741
3742        // Test with no limit - should return all 3 transactions
3743        let mut accumulated = 0;
3744        let mut elements = Vec::new();
3745        pool.append_pooled_transaction_elements(
3746            &[tx0_hash, tx1_hash, tx2_hash],
3747            GetPooledTransactionLimit::None,
3748            &mut accumulated,
3749            &mut elements,
3750        );
3751        assert_eq!(elements.len(), 3, "Should return all 3 transactions");
3752        assert_eq!(
3753            accumulated,
3754            tx0_len + tx1_len + tx2_len,
3755            "Should accumulate all sizes"
3756        );
3757
3758        // Test with a soft limit - stops after exceeding (not at) the limit
3759        // A limit of tx0_len - 1 means we stop after tx0 is added (since tx0_len > limit)
3760        let mut accumulated = 0;
3761        let mut elements = Vec::new();
3762        pool.append_pooled_transaction_elements(
3763            &[tx0_hash, tx1_hash, tx2_hash],
3764            GetPooledTransactionLimit::ResponseSizeSoftLimit(tx0_len - 1),
3765            &mut accumulated,
3766            &mut elements,
3767        );
3768        assert_eq!(
3769            elements.len(),
3770            1,
3771            "Should stop after first tx exceeds limit"
3772        );
3773        assert_eq!(accumulated, tx0_len, "Should accumulate first tx size");
3774
3775        // Test with limit that allows exactly 2 transactions before exceeding
3776        // A limit of tx0_len + tx1_len - 1 means we stop after tx1 is added
3777        let mut accumulated = 0;
3778        let mut elements = Vec::new();
3779        pool.append_pooled_transaction_elements(
3780            &[tx0_hash, tx1_hash, tx2_hash],
3781            GetPooledTransactionLimit::ResponseSizeSoftLimit(tx0_len + tx1_len - 1),
3782            &mut accumulated,
3783            &mut elements,
3784        );
3785        assert_eq!(
3786            elements.len(),
3787            2,
3788            "Should stop after second tx exceeds limit"
3789        );
3790        assert_eq!(
3791            accumulated,
3792            tx0_len + tx1_len,
3793            "Should accumulate first two tx sizes"
3794        );
3795
3796        // Test with pre-accumulated size that causes immediate stop after first tx
3797        let mut accumulated = tx0_len;
3798        let mut elements = Vec::new();
3799        pool.append_pooled_transaction_elements(
3800            &[tx1_hash, tx2_hash],
3801            GetPooledTransactionLimit::ResponseSizeSoftLimit(tx0_len + tx1_len - 1),
3802            &mut accumulated,
3803            &mut elements,
3804        );
3805        assert_eq!(
3806            elements.len(),
3807            1,
3808            "Should return 1 transaction when pre-accumulated size causes early stop"
3809        );
3810        assert_eq!(
3811            accumulated,
3812            tx0_len + tx1_len,
3813            "Should add to pre-accumulated size"
3814        );
3815    }
3816    // ============================================
3817    // Helper function tests
3818    // ============================================
3819
3820    #[test]
3821    fn test_2d_pool_helpers() {
3822        let mut pool = AA2dPool::default();
3823        let sender = Address::random();
3824        let tx = TxBuilder::aa(sender).build();
3825        let tx_hash = *tx.hash();
3826
3827        assert!(!pool.contains(&tx_hash));
3828        assert!(pool.get(&tx_hash).is_none());
3829
3830        pool.add_transaction(
3831            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3832            0,
3833            TempoHardfork::T1,
3834        )
3835        .unwrap();
3836
3837        assert!(pool.contains(&tx_hash));
3838        let retrieved = pool.get(&tx_hash);
3839        assert!(retrieved.is_some());
3840        assert_eq!(retrieved.unwrap().hash(), &tx_hash);
3841    }
3842
3843    #[test]
3844    fn test_pool_get_all() {
3845        let mut pool = AA2dPool::default();
3846        let sender = Address::random();
3847
3848        let tx0 = TxBuilder::aa(sender).build();
3849        let tx1 = TxBuilder::aa(sender).nonce(1).build();
3850        let tx0_hash = *tx0.hash();
3851        let tx1_hash = *tx1.hash();
3852        let fake_hash = alloy_primitives::B256::random();
3853
3854        pool.add_transaction(
3855            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
3856            0,
3857            TempoHardfork::T1,
3858        )
3859        .unwrap();
3860        pool.add_transaction(
3861            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3862            0,
3863            TempoHardfork::T1,
3864        )
3865        .unwrap();
3866
3867        let hashes = [tx0_hash, tx1_hash, fake_hash];
3868        let results = pool.get_all(hashes.iter());
3869
3870        assert_eq!(results.len(), 2); // Only the two real transactions
3871    }
3872
3873    #[test]
3874    fn test_pool_senders_iter() {
3875        let mut pool = AA2dPool::default();
3876        let sender1 = Address::random();
3877        let sender2 = Address::random();
3878
3879        let tx1 = TxBuilder::aa(sender1).build();
3880        let tx2 = TxBuilder::aa(sender2).nonce_key(U256::from(1)).build();
3881
3882        pool.add_transaction(
3883            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3884            0,
3885            TempoHardfork::T1,
3886        )
3887        .unwrap();
3888        pool.add_transaction(
3889            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
3890            0,
3891            TempoHardfork::T1,
3892        )
3893        .unwrap();
3894
3895        let senders: Vec<_> = pool.senders_iter().collect();
3896        assert_eq!(senders.len(), 2);
3897        assert!(senders.contains(&&sender1));
3898        assert!(senders.contains(&&sender2));
3899    }
3900
3901    #[test]
3902    fn test_pool_pending_and_queued_transactions() {
3903        let mut pool = AA2dPool::default();
3904        let sender = Address::random();
3905
3906        // Pending: tx0, tx1, tx2 (consecutive nonces starting from on-chain nonce 0)
3907        let tx0 = TxBuilder::aa(sender).build();
3908        let tx1 = TxBuilder::aa(sender).nonce(1).build();
3909        let tx2 = TxBuilder::aa(sender).nonce(2).build();
3910        let tx0_hash = *tx0.hash();
3911        let tx1_hash = *tx1.hash();
3912        let tx2_hash = *tx2.hash();
3913
3914        // Queued: tx5, tx6, tx7 (gap after tx2)
3915        let tx5 = TxBuilder::aa(sender).nonce(5).build();
3916        let tx6 = TxBuilder::aa(sender).nonce(6).build();
3917        let tx7 = TxBuilder::aa(sender).nonce(7).build();
3918        let tx5_hash = *tx5.hash();
3919        let tx6_hash = *tx6.hash();
3920        let tx7_hash = *tx7.hash();
3921
3922        for tx in [tx0, tx1, tx2, tx5, tx6, tx7] {
3923            pool.add_transaction(
3924                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3925                0,
3926                TempoHardfork::T1,
3927            )
3928            .unwrap();
3929        }
3930
3931        let pending: Vec<_> = pool.pending_transactions().collect();
3932        assert_eq!(pending.len(), 3);
3933        let pending_hashes: HashSet<_> = pending.iter().map(|tx| *tx.hash()).collect();
3934        assert!(pending_hashes.contains(&tx0_hash));
3935        assert!(pending_hashes.contains(&tx1_hash));
3936        assert!(pending_hashes.contains(&tx2_hash));
3937
3938        let queued: Vec<_> = pool.queued_transactions().collect();
3939        assert_eq!(queued.len(), 3);
3940        let queued_hashes: HashSet<_> = queued.iter().map(|tx| *tx.hash()).collect();
3941        assert!(queued_hashes.contains(&tx5_hash));
3942        assert!(queued_hashes.contains(&tx6_hash));
3943        assert!(queued_hashes.contains(&tx7_hash));
3944    }
3945
3946    #[test]
3947    fn test_append_all_transactions() {
3948        let mut pool = AA2dPool::default();
3949        let sender = Address::random();
3950        let expiring_sender = Address::random();
3951
3952        let tx0 = TxBuilder::aa(sender).build();
3953        let tx2 = TxBuilder::aa(sender).nonce(2).build();
3954        let expiring_tx = TxBuilder::aa(expiring_sender).nonce_key(U256::MAX).build();
3955
3956        for tx in [tx0, tx2, expiring_tx] {
3957            pool.add_transaction(
3958                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
3959                0,
3960                TempoHardfork::T1,
3961            )
3962            .unwrap();
3963        }
3964
3965        let expected_pending: HashSet<_> =
3966            pool.pending_transactions().map(|tx| *tx.hash()).collect();
3967        let expected_queued: HashSet<_> = pool.queued_transactions().map(|tx| *tx.hash()).collect();
3968
3969        let mut transactions = AllPoolTransactions::default();
3970        pool.append_all_transactions(&mut transactions);
3971
3972        let pending_hashes: HashSet<_> = transactions.pending.iter().map(|tx| *tx.hash()).collect();
3973        let queued_hashes: HashSet<_> = transactions.queued.iter().map(|tx| *tx.hash()).collect();
3974
3975        assert_eq!(pending_hashes, expected_pending);
3976        assert_eq!(queued_hashes, expected_queued);
3977    }
3978
3979    #[test]
3980    fn test_pool_get_transactions_by_sender_iter() {
3981        let mut pool = AA2dPool::default();
3982        let sender1 = Address::random();
3983        let sender2 = Address::random();
3984
3985        let tx1 = TxBuilder::aa(sender1).nonce_key(U256::ZERO).build();
3986        let tx2 = TxBuilder::aa(sender2).nonce_key(U256::from(1)).build();
3987
3988        pool.add_transaction(
3989            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
3990            0,
3991            TempoHardfork::T1,
3992        )
3993        .unwrap();
3994        pool.add_transaction(
3995            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
3996            0,
3997            TempoHardfork::T1,
3998        )
3999        .unwrap();
4000
4001        let sender1_txs: Vec<_> = pool.get_transactions_by_sender_iter(sender1).collect();
4002        assert_eq!(sender1_txs.len(), 1);
4003        assert_eq!(sender1_txs[0].sender(), sender1);
4004
4005        let sender2_txs: Vec<_> = pool.get_transactions_by_sender_iter(sender2).collect();
4006        assert_eq!(sender2_txs.len(), 1);
4007        assert_eq!(sender2_txs[0].sender(), sender2);
4008    }
4009
4010    #[test]
4011    fn test_pool_get_transactions_by_origin_iter() {
4012        let mut pool = AA2dPool::default();
4013        let sender = Address::random();
4014
4015        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4016        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4017
4018        pool.add_transaction(
4019            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4020            0,
4021            TempoHardfork::T1,
4022        )
4023        .unwrap();
4024        pool.add_transaction(
4025            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::External)),
4026            0,
4027            TempoHardfork::T1,
4028        )
4029        .unwrap();
4030
4031        let local_txs: Vec<_> = pool
4032            .get_transactions_by_origin_iter(TransactionOrigin::Local)
4033            .collect();
4034        assert_eq!(local_txs.len(), 1);
4035
4036        let external_txs: Vec<_> = pool
4037            .get_transactions_by_origin_iter(TransactionOrigin::External)
4038            .collect();
4039        assert_eq!(external_txs.len(), 1);
4040    }
4041
4042    #[test]
4043    fn test_pool_get_pending_transactions_by_origin_iter() {
4044        let mut pool = AA2dPool::default();
4045        let sender = Address::random();
4046
4047        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4048        let tx2 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(2).build(); // Queued due to gap
4049
4050        pool.add_transaction(
4051            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4052            0,
4053            TempoHardfork::T1,
4054        )
4055        .unwrap();
4056        pool.add_transaction(
4057            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
4058            0,
4059            TempoHardfork::T1,
4060        )
4061        .unwrap();
4062
4063        let pending_local: Vec<_> = pool
4064            .get_pending_transactions_by_origin_iter(TransactionOrigin::Local)
4065            .collect();
4066        assert_eq!(pending_local.len(), 1); // Only tx0 is pending
4067    }
4068
4069    #[test]
4070    fn test_pool_all_transaction_hashes_iter() {
4071        let mut pool = AA2dPool::default();
4072        let sender = Address::random();
4073
4074        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4075        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4076        let tx0_hash = *tx0.hash();
4077        let tx1_hash = *tx1.hash();
4078
4079        pool.add_transaction(
4080            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4081            0,
4082            TempoHardfork::T1,
4083        )
4084        .unwrap();
4085        pool.add_transaction(
4086            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4087            0,
4088            TempoHardfork::T1,
4089        )
4090        .unwrap();
4091
4092        let hashes: Vec<_> = pool.all_transaction_hashes_iter().collect();
4093        assert_eq!(hashes.len(), 2);
4094        assert!(hashes.contains(&tx0_hash));
4095        assert!(hashes.contains(&tx1_hash));
4096    }
4097
4098    #[test]
4099    fn test_pool_pooled_transactions_hashes_iter() {
4100        let mut pool = AA2dPool::default();
4101        let sender = Address::random();
4102
4103        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4104        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4105
4106        pool.add_transaction(
4107            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4108            0,
4109            TempoHardfork::T1,
4110        )
4111        .unwrap();
4112        pool.add_transaction(
4113            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4114            0,
4115            TempoHardfork::T1,
4116        )
4117        .unwrap();
4118
4119        let hashes: Vec<_> = pool.pooled_transactions_hashes_iter().collect();
4120        assert_eq!(hashes.len(), 2);
4121    }
4122
4123    #[test]
4124    fn test_pool_pooled_transactions_iter() {
4125        let mut pool = AA2dPool::default();
4126        let sender = Address::random();
4127
4128        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4129        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4130
4131        pool.add_transaction(
4132            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4133            0,
4134            TempoHardfork::T1,
4135        )
4136        .unwrap();
4137        pool.add_transaction(
4138            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4139            0,
4140            TempoHardfork::T1,
4141        )
4142        .unwrap();
4143
4144        let txs: Vec<_> = pool.pooled_transactions_iter().collect();
4145        assert_eq!(txs.len(), 2);
4146    }
4147
4148    // ============================================
4149    // BestAA2dTransactions tests
4150    // ============================================
4151
4152    #[test]
4153    fn test_best_transactions_iterator() {
4154        let mut pool = AA2dPool::default();
4155        let sender = Address::random();
4156
4157        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4158        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4159
4160        pool.add_transaction(
4161            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4162            0,
4163            TempoHardfork::T1,
4164        )
4165        .unwrap();
4166        pool.add_transaction(
4167            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4168            0,
4169            TempoHardfork::T1,
4170        )
4171        .unwrap();
4172
4173        let mut best = pool.best_transactions();
4174
4175        // Should iterate through pending transactions
4176        let first = best.next();
4177        assert!(first.is_some());
4178
4179        let second = best.next();
4180        assert!(second.is_some());
4181
4182        let third = best.next();
4183        assert!(third.is_none());
4184    }
4185
4186    #[test]
4187    fn test_best_transactions_size_hint_counts_snapshot() {
4188        let mut pool = AA2dPool::default();
4189        let sender = Address::random();
4190        let expiring_sender = Address::random();
4191
4192        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4193        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4194        let expiring_tx = TxBuilder::aa(expiring_sender).nonce_key(U256::MAX).build();
4195
4196        pool.add_transaction(
4197            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4198            0,
4199            TempoHardfork::T1,
4200        )
4201        .unwrap();
4202        pool.add_transaction(
4203            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4204            0,
4205            TempoHardfork::T1,
4206        )
4207        .unwrap();
4208        pool.add_transaction(
4209            Arc::new(wrap_valid_tx(expiring_tx, TransactionOrigin::Local)),
4210            0,
4211            TempoHardfork::T1,
4212        )
4213        .unwrap();
4214
4215        let mut best = pool.best_transactions();
4216
4217        assert_eq!(best.size_hint(), (0, Some(3)));
4218        assert!(best.next().is_some());
4219        assert_eq!(best.size_hint(), (0, Some(2)));
4220        assert!(best.next().is_some());
4221        assert_eq!(best.size_hint(), (0, Some(1)));
4222        assert!(best.next().is_some());
4223        assert_eq!(best.size_hint(), (0, Some(0)));
4224    }
4225
4226    #[test]
4227    fn test_best_transactions_size_hint_ignores_unread_new_transactions() {
4228        let mut pool = AA2dPool::default();
4229        let snapshot_sender = Address::random();
4230        let incoming_sender = Address::random();
4231
4232        let snapshot_tx = TxBuilder::aa(snapshot_sender).nonce_key(U256::ZERO).build();
4233        let incoming_tx = TxBuilder::aa(incoming_sender)
4234            .nonce_key(U256::from(1))
4235            .build();
4236
4237        pool.add_transaction(
4238            Arc::new(wrap_valid_tx(snapshot_tx, TransactionOrigin::Local)),
4239            0,
4240            TempoHardfork::T1,
4241        )
4242        .unwrap();
4243
4244        let best = pool.best_transactions();
4245
4246        pool.add_transaction(
4247            Arc::new(wrap_valid_tx(incoming_tx, TransactionOrigin::Local)),
4248            0,
4249            TempoHardfork::T1,
4250        )
4251        .unwrap();
4252
4253        assert_eq!(best.size_hint(), (0, Some(1)));
4254    }
4255
4256    #[test]
4257    fn test_best_transactions_mark_invalid() {
4258        use reth_primitives_traits::transaction::error::InvalidTransactionError;
4259
4260        let mut pool = AA2dPool::default();
4261        let sender = Address::random();
4262
4263        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4264        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4265
4266        pool.add_transaction(
4267            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4268            0,
4269            TempoHardfork::T1,
4270        )
4271        .unwrap();
4272        pool.add_transaction(
4273            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4274            0,
4275            TempoHardfork::T1,
4276        )
4277        .unwrap();
4278
4279        let mut best = pool.best_transactions();
4280
4281        let first = best.next().unwrap();
4282
4283        // Mark it invalid
4284        let error = reth_transaction_pool::error::InvalidPoolTransactionError::Consensus(
4285            InvalidTransactionError::TxTypeNotSupported,
4286        );
4287        best.mark_invalid(&first, error);
4288
4289        // The sequence should be in the invalid set, so next tx from same sender should be skipped
4290        // But since we already consumed tx0, we'd get tx1 next - but the sequence is now invalid
4291    }
4292
4293    #[test]
4294    fn test_best_transactions_expiring_nonce_independent() {
4295        // Expiring nonce transactions (nonce_key == U256::MAX) are always independent
4296        // and should not trigger unlock logic for dependent transactions
4297        let mut pool = AA2dPool::default();
4298        let sender = Address::random();
4299
4300        // Add expiring nonce transaction
4301        let tx = TxBuilder::aa(sender).nonce_key(U256::MAX).nonce(0).build();
4302        pool.add_transaction(
4303            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4304            0,
4305            TempoHardfork::T1,
4306        )
4307        .unwrap();
4308
4309        let mut best = pool.best_transactions();
4310
4311        // Should return the transaction
4312        let first = best.next();
4313        assert!(first.is_some());
4314
4315        // No more transactions
4316        assert!(best.next().is_none());
4317    }
4318
4319    // ============================================
4320    // Remove transactions tests
4321    // ============================================
4322
4323    #[test]
4324    fn test_remove_transactions_by_sender() {
4325        let mut pool = AA2dPool::default();
4326        let sender1 = Address::random();
4327        let sender2 = Address::random();
4328
4329        let tx1 = TxBuilder::aa(sender1).nonce_key(U256::ZERO).build();
4330        let tx2 = TxBuilder::aa(sender2).nonce_key(U256::from(1)).build();
4331
4332        pool.add_transaction(
4333            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4334            0,
4335            TempoHardfork::T1,
4336        )
4337        .unwrap();
4338        pool.add_transaction(
4339            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
4340            0,
4341            TempoHardfork::T1,
4342        )
4343        .unwrap();
4344
4345        let removed = pool.remove_transactions_by_sender(sender1);
4346        assert_eq!(removed.len(), 1);
4347        assert_eq!(removed[0].sender(), sender1);
4348
4349        // sender1's tx should be gone, sender2's should remain
4350        let (pending, queued) = pool.pending_and_queued_txn_count();
4351        assert_eq!(pending + queued, 1);
4352
4353        pool.assert_invariants();
4354    }
4355
4356    #[test]
4357    fn test_remove_transactions_and_descendants() {
4358        let mut pool = AA2dPool::default();
4359        let sender = Address::random();
4360
4361        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4362        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4363        let tx2 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(2).build();
4364        let tx0_hash = *tx0.hash();
4365
4366        pool.add_transaction(
4367            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4368            0,
4369            TempoHardfork::T1,
4370        )
4371        .unwrap();
4372        pool.add_transaction(
4373            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4374            0,
4375            TempoHardfork::T1,
4376        )
4377        .unwrap();
4378        pool.add_transaction(
4379            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
4380            0,
4381            TempoHardfork::T1,
4382        )
4383        .unwrap();
4384
4385        // Remove tx0 and its descendants (tx1, tx2)
4386        let removed = pool.remove_transactions_and_descendants([&tx0_hash].into_iter());
4387        assert_eq!(removed.len(), 3);
4388
4389        let (pending, queued) = pool.pending_and_queued_txn_count();
4390        assert_eq!(pending + queued, 0);
4391
4392        pool.assert_invariants();
4393    }
4394
4395    // ============================================
4396    // AASequenceId and AA2dTransactionId tests
4397    // ============================================
4398
4399    #[test]
4400    fn test_aa_sequence_id_equality() {
4401        let addr = Address::random();
4402        let nonce_key = U256::from(42);
4403
4404        let id1 = AASequenceId::new(addr, nonce_key);
4405        let id2 = AASequenceId::new(addr, nonce_key);
4406        let id3 = AASequenceId::new(Address::random(), nonce_key);
4407
4408        assert_eq!(id1, id2);
4409        assert_ne!(id1, id3);
4410    }
4411
4412    #[test]
4413    fn test_aa2d_transaction_id_unlocks() {
4414        let addr = Address::random();
4415        let seq_id = AASequenceId::new(addr, U256::ZERO);
4416        let tx_id = AA2dTransactionId::new(seq_id, 5);
4417
4418        let next_id = tx_id.unlocks();
4419        assert_eq!(next_id.seq_id, seq_id);
4420        assert_eq!(next_id.nonce, 6);
4421    }
4422
4423    #[test]
4424    fn test_aa2d_transaction_id_ordering() {
4425        let addr = Address::random();
4426        let seq_id = AASequenceId::new(addr, U256::ZERO);
4427
4428        let id1 = AA2dTransactionId::new(seq_id, 1);
4429        let id2 = AA2dTransactionId::new(seq_id, 2);
4430
4431        assert!(id1 < id2);
4432    }
4433
4434    // ============================================
4435    // Edge case tests
4436    // ============================================
4437
4438    #[test]
4439    fn test_nonce_overflow_at_u64_max() {
4440        let mut pool = AA2dPool::default();
4441        let sender = Address::random();
4442        let nonce_key = U256::ZERO;
4443
4444        let tx = TxBuilder::aa(sender)
4445            .nonce_key(nonce_key)
4446            .nonce(u64::MAX)
4447            .build();
4448        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
4449
4450        let result = pool.add_transaction(Arc::new(valid_tx), u64::MAX, TempoHardfork::T1);
4451        assert!(result.is_ok());
4452
4453        let (pending, queued) = pool.pending_and_queued_txn_count();
4454        assert_eq!(pending, 1);
4455        assert_eq!(queued, 0);
4456
4457        let seq_id = AASequenceId::new(sender, nonce_key);
4458        let tx_id = AA2dTransactionId::new(seq_id, u64::MAX);
4459        let unlocked = tx_id.unlocks();
4460        assert_eq!(
4461            unlocked.nonce,
4462            u64::MAX,
4463            "saturating_add should not overflow"
4464        );
4465
4466        pool.assert_invariants();
4467    }
4468
4469    #[test]
4470    fn test_nonce_near_max_with_gap() {
4471        let mut pool = AA2dPool::default();
4472        let sender = Address::random();
4473        let nonce_key = U256::ZERO;
4474
4475        let tx_max = TxBuilder::aa(sender)
4476            .nonce_key(nonce_key)
4477            .nonce(u64::MAX)
4478            .build();
4479        let tx_max_minus_1 = TxBuilder::aa(sender)
4480            .nonce_key(nonce_key)
4481            .nonce(u64::MAX - 1)
4482            .build();
4483
4484        pool.add_transaction(
4485            Arc::new(wrap_valid_tx(tx_max, TransactionOrigin::Local)),
4486            u64::MAX - 1,
4487            TempoHardfork::T1,
4488        )
4489        .unwrap();
4490
4491        let (pending, queued) = pool.pending_and_queued_txn_count();
4492        assert_eq!(pending, 0, "tx at u64::MAX should be queued (gap exists)");
4493        assert_eq!(queued, 1);
4494
4495        pool.add_transaction(
4496            Arc::new(wrap_valid_tx(tx_max_minus_1, TransactionOrigin::Local)),
4497            u64::MAX - 1,
4498            TempoHardfork::T1,
4499        )
4500        .unwrap();
4501
4502        let (pending, queued) = pool.pending_and_queued_txn_count();
4503        assert_eq!(pending, 2, "both should now be pending");
4504        assert_eq!(queued, 0);
4505
4506        pool.assert_invariants();
4507    }
4508
4509    #[test]
4510    fn test_empty_pool_operations() {
4511        let pool = AA2dPool::default();
4512
4513        assert_eq!(pool.pending_and_queued_txn_count(), (0, 0));
4514        assert!(pool.get(&B256::random()).is_none());
4515        assert!(!pool.contains(&B256::random()));
4516        assert_eq!(pool.senders_iter().count(), 0);
4517        assert_eq!(pool.pending_transactions().count(), 0);
4518        assert_eq!(pool.queued_transactions().count(), 0);
4519        assert_eq!(pool.all_transaction_hashes_iter().count(), 0);
4520        assert_eq!(pool.pooled_transactions_hashes_iter().count(), 0);
4521        assert_eq!(pool.pooled_transactions_iter().count(), 0);
4522
4523        let mut best = pool.best_transactions();
4524        assert!(best.next().is_none());
4525    }
4526
4527    #[test]
4528    fn test_empty_pool_remove_operations() {
4529        let mut pool = AA2dPool::default();
4530        let random_hash = B256::random();
4531        let random_sender = Address::random();
4532
4533        let removed = pool.remove_transactions([&random_hash].into_iter());
4534        assert!(removed.is_empty());
4535
4536        let removed = pool.remove_transactions_by_sender(random_sender);
4537        assert!(removed.is_empty());
4538
4539        let removed = pool.remove_transactions_and_descendants([&random_hash].into_iter());
4540        assert!(removed.is_empty());
4541
4542        pool.assert_invariants();
4543    }
4544
4545    #[test]
4546    fn test_empty_pool_on_nonce_changes() {
4547        let mut pool = AA2dPool::default();
4548
4549        let mut changes = HashMap::default();
4550        changes.insert(AASequenceId::new(Address::random(), U256::ZERO), 5u64);
4551
4552        let (promoted, mined) = pool.on_nonce_changes(changes);
4553        assert!(promoted.is_empty());
4554        assert!(mined.is_empty());
4555
4556        pool.assert_invariants();
4557    }
4558
4559    // ============================================
4560    // Error path tests
4561    // ============================================
4562
4563    #[test]
4564    fn test_add_already_imported_transaction() {
4565        let mut pool = AA2dPool::default();
4566        let sender = Address::random();
4567
4568        let tx = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4569        let tx_hash = *tx.hash();
4570        let valid_tx = Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local));
4571
4572        pool.add_transaction(valid_tx.clone(), 0, TempoHardfork::T1)
4573            .unwrap();
4574
4575        let result = pool.add_transaction(valid_tx, 0, TempoHardfork::T1);
4576        assert!(result.is_err());
4577        let err = result.unwrap_err();
4578        assert_eq!(err.hash, tx_hash);
4579        assert!(
4580            matches!(err.kind, PoolErrorKind::AlreadyImported),
4581            "Expected AlreadyImported, got {:?}",
4582            err.kind
4583        );
4584
4585        pool.assert_invariants();
4586    }
4587
4588    #[test]
4589    fn test_add_outdated_nonce_transaction() {
4590        let mut pool = AA2dPool::default();
4591        let sender = Address::random();
4592
4593        let tx = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(5).build();
4594        let tx_hash = *tx.hash();
4595        let valid_tx = Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local));
4596
4597        let result = pool.add_transaction(valid_tx, 10, TempoHardfork::T1);
4598        assert!(result.is_err());
4599        let err = result.unwrap_err();
4600        assert_eq!(err.hash, tx_hash);
4601        assert!(
4602            matches!(
4603                err.kind,
4604                PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus(
4605                    InvalidTransactionError::NonceNotConsistent { tx: 5, state: 10 }
4606                ))
4607            ),
4608            "Expected NonceNotConsistent, got {:?}",
4609            err.kind
4610        );
4611
4612        let (pending, queued) = pool.pending_and_queued_txn_count();
4613        assert_eq!(pending + queued, 0);
4614    }
4615
4616    #[test]
4617    fn test_replacement_underpriced() {
4618        let mut pool = AA2dPool::default();
4619        let sender = Address::random();
4620
4621        let tx1 = TxBuilder::aa(sender)
4622            .nonce_key(U256::ZERO)
4623            .max_priority_fee(1_000_000_000)
4624            .max_fee(2_000_000_000)
4625            .build();
4626        pool.add_transaction(
4627            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4628            0,
4629            TempoHardfork::T1,
4630        )
4631        .unwrap();
4632
4633        let tx2 = TxBuilder::aa(sender)
4634            .nonce_key(U256::ZERO)
4635            .max_priority_fee(1_000_000_001)
4636            .max_fee(2_000_000_001)
4637            .build();
4638        let tx2_hash = *tx2.hash();
4639        let result = pool.add_transaction(
4640            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
4641            0,
4642            TempoHardfork::T1,
4643        );
4644
4645        assert!(result.is_err());
4646        let err = result.unwrap_err();
4647        assert_eq!(err.hash, tx2_hash);
4648        assert!(
4649            matches!(err.kind, PoolErrorKind::ReplacementUnderpriced),
4650            "Expected ReplacementUnderpriced, got {:?}",
4651            err.kind
4652        );
4653
4654        let (pending, queued) = pool.pending_and_queued_txn_count();
4655        assert_eq!(pending + queued, 1);
4656
4657        pool.assert_invariants();
4658    }
4659
4660    // ============================================
4661    // Boundary tests (max_txs limit and discard)
4662    // ============================================
4663
4664    #[test]
4665    fn test_discard_at_max_txs_limit() {
4666        let config = AA2dPoolConfig {
4667            price_bump_config: PriceBumpConfig::default(),
4668            pending_limit: SubPoolLimit {
4669                max_txs: 3,
4670                max_size: usize::MAX,
4671            },
4672            queued_limit: SubPoolLimit {
4673                max_txs: 10000,
4674                max_size: usize::MAX,
4675            },
4676            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4677        };
4678        let mut pool = AA2dPool::new(config);
4679
4680        for i in 0..5usize {
4681            let sender = Address::from_word(B256::from(U256::from(i)));
4682            let tx = TxBuilder::aa(sender).nonce_key(U256::from(i)).build();
4683            let result = pool.add_transaction(
4684                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4685                0,
4686                TempoHardfork::T1,
4687            );
4688            assert!(result.is_ok());
4689        }
4690
4691        let (pending, queued) = pool.pending_and_queued_txn_count();
4692        assert_eq!(pending + queued, 3, "Pool should be capped at max_txs=3");
4693        assert_eq!(pending, 3, "All remaining transactions should be pending");
4694
4695        pool.assert_invariants();
4696    }
4697
4698    #[test]
4699    fn test_discard_removes_lowest_priority_same_priority_uses_submission_order() {
4700        let config = AA2dPoolConfig {
4701            price_bump_config: PriceBumpConfig::default(),
4702            pending_limit: SubPoolLimit {
4703                max_txs: 2,
4704                max_size: usize::MAX,
4705            },
4706            queued_limit: SubPoolLimit {
4707                max_txs: 10000,
4708                max_size: usize::MAX,
4709            },
4710            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4711        };
4712        let mut pool = AA2dPool::new(config);
4713        let sender = Address::random();
4714
4715        // All transactions have the same priority, so the tiebreaker is submission order.
4716        // The most recently submitted (tx2) should be evicted first.
4717        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
4718        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
4719        let tx2 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(2).build();
4720        let tx0_hash = *tx0.hash();
4721        let tx1_hash = *tx1.hash();
4722        let tx2_hash = *tx2.hash();
4723
4724        pool.add_transaction(
4725            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
4726            0,
4727            TempoHardfork::T1,
4728        )
4729        .unwrap();
4730        pool.add_transaction(
4731            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
4732            0,
4733            TempoHardfork::T1,
4734        )
4735        .unwrap();
4736        let result = pool.add_transaction(
4737            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
4738            0,
4739            TempoHardfork::T1,
4740        );
4741        assert!(result.is_ok());
4742
4743        let added = result.unwrap();
4744        if let AddedTransaction::Pending(pending) = added {
4745            assert!(
4746                !pending.discarded.is_empty(),
4747                "Should have discarded transactions"
4748            );
4749            assert_eq!(
4750                pending.discarded[0].hash(),
4751                &tx2_hash,
4752                "tx2 (last submitted, lowest priority tiebreaker) should be discarded"
4753            );
4754        } else {
4755            panic!("Expected Pending result");
4756        }
4757
4758        assert!(pool.contains(&tx0_hash));
4759        assert!(pool.contains(&tx1_hash));
4760        assert!(!pool.contains(&tx2_hash));
4761
4762        pool.assert_invariants();
4763    }
4764
4765    /// Tests that queued transactions (with nonce gaps) also respect the max_txs limit.
4766    #[test]
4767    fn test_discard_enforced_for_queued_transactions() {
4768        let config = AA2dPoolConfig {
4769            price_bump_config: PriceBumpConfig::default(),
4770            pending_limit: SubPoolLimit {
4771                max_txs: 2,
4772                max_size: usize::MAX,
4773            },
4774            queued_limit: SubPoolLimit {
4775                max_txs: 2,
4776                max_size: usize::MAX,
4777            },
4778            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4779        };
4780        let mut pool = AA2dPool::new(config);
4781
4782        // Add 5 transactions each with a LARGE nonce gap so they are all queued
4783        for i in 0..5usize {
4784            let sender = Address::from_word(B256::from(U256::from(i)));
4785            let tx = TxBuilder::aa(sender)
4786                .nonce_key(U256::from(i))
4787                .nonce(1000)
4788                .build();
4789            let result = pool.add_transaction(
4790                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4791                0,
4792                TempoHardfork::T1,
4793            );
4794            assert!(result.is_ok(), "Transaction {i} should be added");
4795        }
4796
4797        let (pending, queued) = pool.pending_and_queued_txn_count();
4798        assert_eq!(
4799            pending + queued,
4800            2,
4801            "Pool should be capped at max_txs=2, but has {pending} pending + {queued} queued",
4802        );
4803
4804        pool.assert_invariants();
4805    }
4806
4807    /// Verifies queued transactions respect their own limit independently
4808    #[test]
4809    fn test_queued_limit_enforced_separately() {
4810        let config = AA2dPoolConfig {
4811            price_bump_config: PriceBumpConfig::default(),
4812            pending_limit: SubPoolLimit {
4813                max_txs: 10,
4814                max_size: usize::MAX,
4815            },
4816            queued_limit: SubPoolLimit {
4817                max_txs: 3,
4818                max_size: usize::MAX,
4819            },
4820            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4821        };
4822        let mut pool = AA2dPool::new(config);
4823
4824        // Add 5 queued transactions (far-future nonces)
4825        for i in 0..5usize {
4826            let sender = Address::from_word(B256::from(U256::from(i)));
4827            let tx = TxBuilder::aa(sender)
4828                .nonce_key(U256::from(i))
4829                .nonce(1000)
4830                .build();
4831            let _ = pool.add_transaction(
4832                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4833                0,
4834                TempoHardfork::T1,
4835            );
4836        }
4837
4838        let (pending, queued) = pool.pending_and_queued_txn_count();
4839        assert_eq!(queued, 3, "Queued should be capped at 3");
4840        assert_eq!(pending, 0, "No pending transactions");
4841        pool.assert_invariants();
4842    }
4843
4844    /// Verifies pending transactions respect their own limit independently
4845    #[test]
4846    fn test_pending_limit_enforced_separately() {
4847        let config = AA2dPoolConfig {
4848            price_bump_config: PriceBumpConfig::default(),
4849            pending_limit: SubPoolLimit {
4850                max_txs: 3,
4851                max_size: usize::MAX,
4852            },
4853            queued_limit: SubPoolLimit {
4854                max_txs: 10,
4855                max_size: usize::MAX,
4856            },
4857            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4858        };
4859        let mut pool = AA2dPool::new(config);
4860
4861        // Add 5 pending transactions (nonce=0, different senders)
4862        for i in 0..5usize {
4863            let sender = Address::from_word(B256::from(U256::from(i)));
4864            let tx = TxBuilder::aa(sender).nonce_key(U256::from(i)).build();
4865            let _ = pool.add_transaction(
4866                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4867                0,
4868                TempoHardfork::T1,
4869            );
4870        }
4871
4872        let (pending, queued) = pool.pending_and_queued_txn_count();
4873        assert_eq!(pending, 3, "Pending should be capped at 3");
4874        assert_eq!(queued, 0, "No queued transactions");
4875        pool.assert_invariants();
4876    }
4877
4878    /// Verifies queued spam cannot evict pending transactions
4879    #[test]
4880    fn test_queued_eviction_does_not_affect_pending() {
4881        let config = AA2dPoolConfig {
4882            price_bump_config: PriceBumpConfig::default(),
4883            pending_limit: SubPoolLimit {
4884                max_txs: 5,
4885                max_size: usize::MAX,
4886            },
4887            queued_limit: SubPoolLimit {
4888                max_txs: 2,
4889                max_size: usize::MAX,
4890            },
4891            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4892        };
4893        let mut pool = AA2dPool::new(config);
4894
4895        // First add 3 pending transactions
4896        let mut pending_hashes = Vec::new();
4897        for i in 0..3usize {
4898            let sender = Address::from_word(B256::from(U256::from(i)));
4899            let tx = TxBuilder::aa(sender).nonce_key(U256::from(i)).build();
4900            let hash = *tx.hash();
4901            pending_hashes.push(hash);
4902            let _ = pool.add_transaction(
4903                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4904                0,
4905                TempoHardfork::T1,
4906            );
4907        }
4908
4909        // Now flood with 10 queued transactions
4910        for i in 100..110usize {
4911            let sender = Address::from_word(B256::from(U256::from(i)));
4912            let tx = TxBuilder::aa(sender)
4913                .nonce_key(U256::from(i))
4914                .nonce(1000)
4915                .build();
4916            let _ = pool.add_transaction(
4917                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
4918                0,
4919                TempoHardfork::T1,
4920            );
4921        }
4922
4923        // All pending should still be there
4924        for hash in &pending_hashes {
4925            assert!(
4926                pool.contains(hash),
4927                "Pending tx should not be evicted by queued spam"
4928            );
4929        }
4930
4931        let (pending, queued) = pool.pending_and_queued_txn_count();
4932        assert_eq!(pending, 3, "All 3 pending should remain");
4933        assert_eq!(queued, 2, "Queued capped at 2");
4934        pool.assert_invariants();
4935    }
4936
4937    /// Tests that eviction is based on priority, not address ordering.
4938    /// This prevents DoS attacks where adversaries use vanity addresses with leading zeroes.
4939    #[test]
4940    fn test_discard_evicts_low_priority_over_vanity_address() {
4941        let config = AA2dPoolConfig {
4942            price_bump_config: PriceBumpConfig::default(),
4943            pending_limit: SubPoolLimit {
4944                max_txs: 2,
4945                max_size: usize::MAX,
4946            },
4947            queued_limit: SubPoolLimit {
4948                max_txs: 10,
4949                max_size: usize::MAX,
4950            },
4951            max_txs_per_sender: DEFAULT_MAX_TXS_PER_SENDER,
4952        };
4953        let mut pool = AA2dPool::new(config);
4954
4955        // Vanity address with leading zeroes (would sort first lexicographically)
4956        let vanity_sender = Address::from_word(B256::from_slice(&[0u8; 32])); // 0x0000...0000
4957        // Normal address (would sort later lexicographically)
4958        let normal_sender = Address::from_word(B256::from_slice(&[0xff; 32])); // 0xffff...ffff
4959
4960        // max_fee must be > TEMPO_T1_BASE_FEE (20 gwei) for priority calculation to work
4961        // effective_tip = min(max_fee - base_fee, max_priority_fee)
4962        let high_max_fee = 30_000_000_000u128; // 30 gwei, above 20 gwei base fee
4963
4964        // Add vanity address tx with HIGH priority (should be kept despite sorting first lexicographically)
4965        // effective_tip = min(30 gwei - 20 gwei, 5 gwei) = 5 gwei
4966        let high_priority_tx = TxBuilder::aa(vanity_sender)
4967            .nonce_key(U256::ZERO)
4968            .max_fee(high_max_fee)
4969            .max_priority_fee(5_000_000_000) // 5 gwei priority
4970            .build();
4971        let high_priority_hash = *high_priority_tx.hash();
4972
4973        // Add normal address tx with LOW priority (should be evicted)
4974        // effective_tip = min(30 gwei - 20 gwei, 1 wei) = 1 wei
4975        let low_priority_tx = TxBuilder::aa(normal_sender)
4976            .nonce_key(U256::ZERO)
4977            .max_fee(high_max_fee)
4978            .max_priority_fee(1) // Very low priority
4979            .build();
4980        let low_priority_hash = *low_priority_tx.hash();
4981
4982        pool.add_transaction(
4983            Arc::new(wrap_valid_tx(high_priority_tx, TransactionOrigin::Local)),
4984            0,
4985            TempoHardfork::T1,
4986        )
4987        .unwrap();
4988        pool.add_transaction(
4989            Arc::new(wrap_valid_tx(low_priority_tx, TransactionOrigin::Local)),
4990            0,
4991            TempoHardfork::T1,
4992        )
4993        .unwrap();
4994
4995        // Add a third tx that triggers eviction
4996        // effective_tip = min(30 gwei - 20 gwei, 3 gwei) = 3 gwei (medium)
4997        let trigger_tx = TxBuilder::aa(Address::random())
4998            .nonce_key(U256::from(1))
4999            .max_fee(high_max_fee)
5000            .max_priority_fee(3_000_000_000) // 3 gwei - medium priority
5001            .build();
5002        let trigger_hash = *trigger_tx.hash();
5003
5004        let result = pool.add_transaction(
5005            Arc::new(wrap_valid_tx(trigger_tx, TransactionOrigin::Local)),
5006            0,
5007            TempoHardfork::T1,
5008        );
5009        assert!(result.is_ok());
5010
5011        let added = result.unwrap();
5012        if let AddedTransaction::Pending(pending) = added {
5013            assert!(
5014                !pending.discarded.is_empty(),
5015                "Should have discarded transactions"
5016            );
5017            // The low priority tx (normal address) should be evicted, NOT the high priority vanity address
5018            assert_eq!(
5019                pending.discarded[0].hash(),
5020                &low_priority_hash,
5021                "Low priority tx should be evicted, not the high-priority vanity address tx"
5022            );
5023        } else {
5024            panic!("Expected Pending result");
5025        }
5026
5027        // Verify: high priority vanity address tx should be kept, low priority normal address tx should be evicted
5028        assert!(
5029            pool.contains(&high_priority_hash),
5030            "High priority vanity address tx should be kept"
5031        );
5032        assert!(
5033            !pool.contains(&low_priority_hash),
5034            "Low priority tx should be evicted"
5035        );
5036        assert!(pool.contains(&trigger_hash), "Trigger tx should be kept");
5037
5038        pool.assert_invariants();
5039    }
5040
5041    /// Tests that a sender cannot exceed the per-sender transaction limit.
5042    #[test]
5043    fn test_per_sender_limit_rejects_excess_transactions() {
5044        let config = AA2dPoolConfig {
5045            price_bump_config: PriceBumpConfig::default(),
5046            pending_limit: SubPoolLimit {
5047                max_txs: 1000,
5048                max_size: usize::MAX,
5049            },
5050            queued_limit: SubPoolLimit {
5051                max_txs: 1000,
5052                max_size: usize::MAX,
5053            },
5054            max_txs_per_sender: 3,
5055        };
5056        let mut pool = AA2dPool::new(config);
5057        let sender = Address::random();
5058
5059        // Add transactions up to the limit
5060        for nonce in 0..3u64 {
5061            let tx = TxBuilder::aa(sender)
5062                .nonce_key(U256::ZERO)
5063                .nonce(nonce)
5064                .build();
5065            let result = pool.add_transaction(
5066                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5067                0,
5068                TempoHardfork::T1,
5069            );
5070            assert!(result.is_ok(), "Transaction {nonce} should be accepted");
5071        }
5072
5073        // The 4th transaction from the same sender should be rejected
5074        let tx = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(3).build();
5075        let result = pool.add_transaction(
5076            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5077            0,
5078            TempoHardfork::T1,
5079        );
5080        assert!(result.is_err(), "4th transaction should be rejected");
5081        let err = result.unwrap_err();
5082        assert!(
5083            matches!(err.kind, PoolErrorKind::SpammerExceededCapacity(_)),
5084            "Error should be SpammerExceededCapacity, got {:?}",
5085            err.kind
5086        );
5087
5088        // A different sender should still be able to add transactions
5089        let other_sender = Address::random();
5090        let tx = TxBuilder::aa(other_sender).nonce_key(U256::ZERO).build();
5091        let result = pool.add_transaction(
5092            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5093            0,
5094            TempoHardfork::T1,
5095        );
5096        assert!(result.is_ok(), "Different sender should be accepted");
5097
5098        pool.assert_invariants();
5099    }
5100
5101    /// Tests that replacing a transaction doesn't count against the per-sender limit.
5102    #[test]
5103    fn test_per_sender_limit_allows_replacement() {
5104        let config = AA2dPoolConfig {
5105            price_bump_config: PriceBumpConfig::default(),
5106            pending_limit: SubPoolLimit {
5107                max_txs: 1000,
5108                max_size: usize::MAX,
5109            },
5110            queued_limit: SubPoolLimit {
5111                max_txs: 1000,
5112                max_size: usize::MAX,
5113            },
5114            max_txs_per_sender: 2,
5115        };
5116        let mut pool = AA2dPool::new(config);
5117        let sender = Address::random();
5118
5119        // Add 2 transactions to reach the limit
5120        for nonce in 0..2u64 {
5121            let tx = TxBuilder::aa(sender)
5122                .nonce_key(U256::ZERO)
5123                .nonce(nonce)
5124                .build();
5125            pool.add_transaction(
5126                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5127                0,
5128                TempoHardfork::T1,
5129            )
5130            .unwrap();
5131        }
5132
5133        // Replace the first transaction with a higher fee (should succeed)
5134        let replacement_tx = TxBuilder::aa(sender)
5135            .nonce_key(U256::ZERO)
5136            .nonce(0)
5137            .max_fee(100_000_000_000) // Higher fee to pass replacement check
5138            .max_priority_fee(50_000_000_000)
5139            .build();
5140        let result = pool.add_transaction(
5141            Arc::new(wrap_valid_tx(replacement_tx, TransactionOrigin::Local)),
5142            0,
5143            TempoHardfork::T1,
5144        );
5145        assert!(
5146            result.is_ok(),
5147            "Replacement should be allowed even at limit"
5148        );
5149
5150        pool.assert_invariants();
5151    }
5152
5153    /// Tests that removing a transaction frees up a slot for the sender.
5154    #[test]
5155    fn test_per_sender_limit_freed_after_removal() {
5156        let config = AA2dPoolConfig {
5157            price_bump_config: PriceBumpConfig::default(),
5158            pending_limit: SubPoolLimit {
5159                max_txs: 1000,
5160                max_size: usize::MAX,
5161            },
5162            queued_limit: SubPoolLimit {
5163                max_txs: 1000,
5164                max_size: usize::MAX,
5165            },
5166            max_txs_per_sender: 2,
5167        };
5168        let mut pool = AA2dPool::new(config);
5169        let sender = Address::random();
5170
5171        // Add 2 transactions to reach the limit
5172        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(0).build();
5173        let tx1_hash = *tx1.hash();
5174        pool.add_transaction(
5175            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
5176            0,
5177            TempoHardfork::T1,
5178        )
5179        .unwrap();
5180
5181        let tx2 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
5182        pool.add_transaction(
5183            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
5184            0,
5185            TempoHardfork::T1,
5186        )
5187        .unwrap();
5188
5189        // 3rd should fail
5190        let tx3 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(2).build();
5191        let result = pool.add_transaction(
5192            Arc::new(wrap_valid_tx(tx3.clone(), TransactionOrigin::Local)),
5193            0,
5194            TempoHardfork::T1,
5195        );
5196        assert!(result.is_err(), "3rd should be rejected at limit");
5197
5198        // Remove the first transaction
5199        pool.remove_transactions(std::iter::once(&tx1_hash));
5200
5201        // Now adding the 3rd should succeed
5202        let result = pool.add_transaction(
5203            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
5204            0,
5205            TempoHardfork::T1,
5206        );
5207        assert!(result.is_ok(), "3rd should succeed after removal");
5208
5209        pool.assert_invariants();
5210    }
5211
5212    /// Tests that expiring nonce transactions also respect per-sender limits.
5213    #[test]
5214    fn test_per_sender_limit_includes_expiring_nonce_txs() {
5215        let config = AA2dPoolConfig {
5216            price_bump_config: PriceBumpConfig::default(),
5217            pending_limit: SubPoolLimit {
5218                max_txs: 1000,
5219                max_size: usize::MAX,
5220            },
5221            queued_limit: SubPoolLimit {
5222                max_txs: 1000,
5223                max_size: usize::MAX,
5224            },
5225            max_txs_per_sender: 2,
5226        };
5227        let mut pool = AA2dPool::new(config);
5228        let sender = Address::random();
5229
5230        // Add one regular 2D nonce tx
5231        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(0).build();
5232        pool.add_transaction(
5233            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
5234            0,
5235            TempoHardfork::T1,
5236        )
5237        .unwrap();
5238
5239        // Add one expiring nonce tx (nonce_key = U256::MAX)
5240        let tx2 = TxBuilder::aa(sender).nonce_key(U256::MAX).nonce(0).build();
5241        pool.add_transaction(
5242            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
5243            0,
5244            TempoHardfork::T1,
5245        )
5246        .unwrap();
5247
5248        // The 3rd transaction (either type) should be rejected
5249        let tx3 = TxBuilder::aa(sender)
5250            .nonce_key(U256::from(1))
5251            .nonce(0)
5252            .build();
5253        let result = pool.add_transaction(
5254            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
5255            0,
5256            TempoHardfork::T1,
5257        );
5258        assert!(
5259            result.is_err(),
5260            "3rd tx should be rejected due to per-sender limit"
5261        );
5262
5263        pool.assert_invariants();
5264    }
5265
5266    // ============================================
5267    // Improved BestTransactions tests
5268    // ============================================
5269
5270    #[test]
5271    fn test_best_transactions_mark_invalid_skips_sequence() {
5272        use reth_primitives_traits::transaction::error::InvalidTransactionError;
5273
5274        let mut pool = AA2dPool::default();
5275        let sender1 = Address::random();
5276        let sender2 = Address::random();
5277
5278        let tx1_0 = TxBuilder::aa(sender1).nonce_key(U256::ZERO).build();
5279        let tx1_1 = TxBuilder::aa(sender1)
5280            .nonce_key(U256::ZERO)
5281            .nonce(1)
5282            .build();
5283        let tx2_0 = TxBuilder::aa(sender2).nonce_key(U256::from(1)).build();
5284
5285        let tx1_0_hash = *tx1_0.hash();
5286        let tx2_0_hash = *tx2_0.hash();
5287
5288        pool.add_transaction(
5289            Arc::new(wrap_valid_tx(tx1_0, TransactionOrigin::Local)),
5290            0,
5291            TempoHardfork::T1,
5292        )
5293        .unwrap();
5294        pool.add_transaction(
5295            Arc::new(wrap_valid_tx(tx1_1, TransactionOrigin::Local)),
5296            0,
5297            TempoHardfork::T1,
5298        )
5299        .unwrap();
5300        pool.add_transaction(
5301            Arc::new(wrap_valid_tx(tx2_0, TransactionOrigin::Local)),
5302            0,
5303            TempoHardfork::T1,
5304        )
5305        .unwrap();
5306
5307        let mut best = pool.best_transactions();
5308
5309        let first = best.next().unwrap();
5310        let first_hash = *first.hash();
5311
5312        let error =
5313            InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported);
5314        best.mark_invalid(&first, error);
5315
5316        let mut remaining_hashes = HashSet::new();
5317        for tx in best {
5318            remaining_hashes.insert(*tx.hash());
5319        }
5320
5321        if first_hash == tx1_0_hash {
5322            assert!(
5323                !remaining_hashes.contains(&tx1_0_hash),
5324                "tx1_0 was consumed"
5325            );
5326            assert!(
5327                remaining_hashes.contains(&tx2_0_hash),
5328                "tx2_0 should still be yielded"
5329            );
5330        } else {
5331            assert!(
5332                remaining_hashes.contains(&tx1_0_hash) || remaining_hashes.contains(&tx2_0_hash),
5333                "At least one other independent tx should be yielded"
5334            );
5335        }
5336    }
5337
5338    #[test]
5339    fn test_best_transactions_order_by_priority() {
5340        let mut pool = AA2dPool::default();
5341
5342        let sender1 = Address::random();
5343        let sender2 = Address::random();
5344
5345        let low_priority = TxBuilder::aa(sender1)
5346            .nonce_key(U256::ZERO)
5347            .max_priority_fee(1_000_000)
5348            .max_fee(2_000_000)
5349            .build();
5350        let high_priority = TxBuilder::aa(sender2)
5351            .nonce_key(U256::from(1))
5352            .max_priority_fee(10_000_000_000)
5353            .max_fee(20_000_000_000)
5354            .build();
5355        let high_priority_hash = *high_priority.hash();
5356
5357        pool.add_transaction(
5358            Arc::new(wrap_valid_tx(low_priority, TransactionOrigin::Local)),
5359            0,
5360            TempoHardfork::T1,
5361        )
5362        .unwrap();
5363        pool.add_transaction(
5364            Arc::new(wrap_valid_tx(high_priority, TransactionOrigin::Local)),
5365            0,
5366            TempoHardfork::T1,
5367        )
5368        .unwrap();
5369
5370        let mut best = pool.best_transactions();
5371        let first = best.next().unwrap();
5372
5373        assert_eq!(
5374            first.hash(),
5375            &high_priority_hash,
5376            "Higher priority transaction should come first"
5377        );
5378    }
5379
5380    fn priority_flip_pool(block_base_fee: u64) -> (AA2dPool, B256, B256) {
5381        priority_flip_pool_with_config(block_base_fee, AA2dPoolConfig::default())
5382    }
5383
5384    fn priority_flip_pool_with_config(
5385        block_base_fee: u64,
5386        config: AA2dPoolConfig,
5387    ) -> (AA2dPool, B256, B256) {
5388        let mut pool = AA2dPool::new(config);
5389        pool.set_base_fee(TEMPO_T1_BASE_FEE);
5390
5391        let high_at_insert_low_at_block = TxBuilder::aa(Address::random())
5392            .nonce_key(U256::from(1))
5393            .max_priority_fee(10_000_000_000)
5394            .max_fee(u128::from(block_base_fee) + 1)
5395            .build();
5396        let high_at_insert_low_at_block_hash = *high_at_insert_low_at_block.hash();
5397
5398        let low_at_insert_high_at_block = TxBuilder::aa(Address::random())
5399            .nonce_key(U256::from(2))
5400            .max_priority_fee(5_000_000_000)
5401            .max_fee(u128::from(block_base_fee) + 5_000_000_000)
5402            .build();
5403        let low_at_insert_high_at_block_hash = *low_at_insert_high_at_block.hash();
5404
5405        for tx in [high_at_insert_low_at_block, low_at_insert_high_at_block] {
5406            pool.add_transaction(
5407                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5408                0,
5409                TempoHardfork::T1,
5410            )
5411            .unwrap();
5412        }
5413
5414        (
5415            pool,
5416            low_at_insert_high_at_block_hash,
5417            high_at_insert_low_at_block_hash,
5418        )
5419    }
5420
5421    #[test]
5422    fn test_best_transactions_with_base_fee_reprioritizes_regular_transactions() {
5423        let block_base_fee = TEMPO_T1_BASE_FEE + 10_000_000_000;
5424        let (pool, expected_first, expected_second) = priority_flip_pool(block_base_fee);
5425
5426        let hashes = pool
5427            .best_transactions_with_base_fee(block_base_fee)
5428            .map(|tx| *tx.hash())
5429            .collect::<Vec<_>>();
5430
5431        assert_eq!(hashes, vec![expected_first, expected_second]);
5432    }
5433
5434    #[test]
5435    fn test_discard_reprices_eviction_priorities() {
5436        let block_base_fee = TEMPO_T1_BASE_FEE + 10_000_000_000;
5437        let (mut pool, expected_kept, expected_evicted) = priority_flip_pool_with_config(
5438            block_base_fee,
5439            AA2dPoolConfig {
5440                pending_limit: SubPoolLimit {
5441                    max_txs: 2,
5442                    max_size: usize::MAX,
5443                },
5444                queued_limit: SubPoolLimit {
5445                    max_txs: 10,
5446                    max_size: usize::MAX,
5447                },
5448                ..Default::default()
5449            },
5450        );
5451        pool.set_base_fee(block_base_fee);
5452        let trigger = TxBuilder::aa(Address::random())
5453            .nonce_key(U256::from(3))
5454            .max_priority_fee(10_000_000_000)
5455            .max_fee(u128::from(block_base_fee) + 10_000_000_000)
5456            .build();
5457
5458        let result = pool
5459            .add_transaction(
5460                Arc::new(wrap_valid_tx(trigger, TransactionOrigin::Local)),
5461                0,
5462                TempoHardfork::T1,
5463            )
5464            .unwrap();
5465
5466        let AddedTransaction::Pending(pending) = result else {
5467            panic!("expected pending transaction")
5468        };
5469        assert_eq!(pending.discarded[0].hash(), &expected_evicted);
5470        assert!(!pool.contains(&expected_evicted));
5471        assert!(pool.contains(&expected_kept));
5472    }
5473
5474    #[test]
5475    fn test_best_transactions_with_base_fee_filters_underpriced_regular_sequence() {
5476        let mut pool = AA2dPool::default();
5477        let block_base_fee = TEMPO_T1_BASE_FEE + 10_000_000_000;
5478        let sequence_sender = Address::random();
5479
5480        let underpriced_parent = TxBuilder::aa(sequence_sender)
5481            .nonce_key(U256::from(1))
5482            .max_fee(u128::from(block_base_fee - 1))
5483            .max_priority_fee(1_000_000_000)
5484            .build();
5485        let valid_child = TxBuilder::aa(sequence_sender)
5486            .nonce_key(U256::from(1))
5487            .nonce(1)
5488            .max_fee(u128::from(block_base_fee) + 10_000_000_000)
5489            .max_priority_fee(10_000_000_000)
5490            .build();
5491        let valid_independent = TxBuilder::aa(Address::random())
5492            .nonce_key(U256::from(2))
5493            .max_fee(u128::from(block_base_fee) + 1_000_000_000)
5494            .max_priority_fee(1_000_000_000)
5495            .build();
5496        let valid_independent_hash = *valid_independent.hash();
5497
5498        for tx in [underpriced_parent, valid_child, valid_independent] {
5499            pool.add_transaction(
5500                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5501                0,
5502                TempoHardfork::T1,
5503            )
5504            .unwrap();
5505        }
5506
5507        let hashes = pool
5508            .best_transactions_with_base_fee(block_base_fee)
5509            .map(|tx| *tx.hash())
5510            .collect::<Vec<_>>();
5511
5512        assert_eq!(hashes, vec![valid_independent_hash]);
5513    }
5514
5515    #[test]
5516    fn test_best_transactions_with_base_fee_filters_underpriced_expiring_nonce() {
5517        let mut pool = AA2dPool::default();
5518        let block_base_fee = TEMPO_T1_BASE_FEE + 10_000_000_000;
5519
5520        let underpriced = TxBuilder::aa(Address::random())
5521            .nonce_key(U256::MAX)
5522            .max_fee(u128::from(block_base_fee - 1))
5523            .max_priority_fee(1_000_000_000)
5524            .build();
5525        let valid = TxBuilder::aa(Address::random())
5526            .nonce_key(U256::MAX)
5527            .max_fee(u128::from(block_base_fee) + 1_000_000_000)
5528            .max_priority_fee(1_000_000_000)
5529            .build();
5530        let valid_hash = *valid.hash();
5531
5532        for tx in [underpriced, valid] {
5533            pool.add_transaction(
5534                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5535                0,
5536                TempoHardfork::T1,
5537            )
5538            .unwrap();
5539        }
5540
5541        let hashes = pool
5542            .best_transactions_with_base_fee(block_base_fee)
5543            .map(|tx| *tx.hash())
5544            .collect::<Vec<_>>();
5545
5546        assert_eq!(hashes, vec![valid_hash]);
5547    }
5548
5549    #[test]
5550    fn test_best_transactions_merges_regular_and_expiring_by_priority() {
5551        let mut pool = AA2dPool::default();
5552        let max_fee = 30_000_000_000u128;
5553
5554        let regular_low = TxBuilder::aa(Address::random())
5555            .nonce_key(U256::from(1))
5556            .max_fee(max_fee)
5557            .max_priority_fee(1_000_000_000)
5558            .build();
5559        let regular_low_hash = *regular_low.hash();
5560
5561        let expiring_high = TxBuilder::aa(Address::random())
5562            .nonce_key(U256::MAX)
5563            .max_fee(max_fee)
5564            .max_priority_fee(5_000_000_000)
5565            .build();
5566        let expiring_high_hash = *expiring_high.hash();
5567
5568        let regular_mid = TxBuilder::aa(Address::random())
5569            .nonce_key(U256::from(2))
5570            .max_fee(max_fee)
5571            .max_priority_fee(3_000_000_000)
5572            .build();
5573        let regular_mid_hash = *regular_mid.hash();
5574
5575        let expiring_low = TxBuilder::aa(Address::random())
5576            .nonce_key(U256::MAX)
5577            .max_fee(max_fee)
5578            .max_priority_fee(2_000_000_000)
5579            .build();
5580        let expiring_low_hash = *expiring_low.hash();
5581
5582        for tx in [regular_low, expiring_high, regular_mid, expiring_low] {
5583            pool.add_transaction(
5584                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5585                0,
5586                TempoHardfork::T1,
5587            )
5588            .unwrap();
5589        }
5590        pool.assert_invariants();
5591
5592        let hashes = pool
5593            .best_transactions()
5594            .map(|tx| *tx.hash())
5595            .collect::<Vec<_>>();
5596
5597        assert_eq!(
5598            hashes,
5599            vec![
5600                expiring_high_hash,
5601                regular_mid_hash,
5602                expiring_low_hash,
5603                regular_low_hash,
5604            ]
5605        );
5606    }
5607
5608    #[test]
5609    fn test_best_transactions_merges_regular_and_expiring_by_submission_id() {
5610        let max_fee = 30_000_000_000u128;
5611
5612        let mut expiring_older_pool = AA2dPool::default();
5613        let expiring_older = TxBuilder::aa(Address::random())
5614            .nonce_key(U256::MAX)
5615            .max_fee(max_fee)
5616            .max_priority_fee(1_000_000_000)
5617            .build();
5618        let expiring_older_hash = *expiring_older.hash();
5619        let regular_newer = TxBuilder::aa(Address::random())
5620            .nonce_key(U256::from(1))
5621            .max_fee(max_fee)
5622            .max_priority_fee(1_000_000_000)
5623            .build();
5624
5625        for tx in [expiring_older, regular_newer] {
5626            expiring_older_pool
5627                .add_transaction(
5628                    Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5629                    0,
5630                    TempoHardfork::T1,
5631                )
5632                .unwrap();
5633        }
5634
5635        assert_eq!(
5636            expiring_older_pool
5637                .best_transactions()
5638                .next()
5639                .unwrap()
5640                .hash(),
5641            &expiring_older_hash
5642        );
5643
5644        let mut regular_older_pool = AA2dPool::default();
5645        let regular_older = TxBuilder::aa(Address::random())
5646            .nonce_key(U256::from(1))
5647            .max_fee(max_fee)
5648            .max_priority_fee(1_000_000_000)
5649            .build();
5650        let regular_older_hash = *regular_older.hash();
5651        let expiring_newer = TxBuilder::aa(Address::random())
5652            .nonce_key(U256::MAX)
5653            .max_fee(max_fee)
5654            .max_priority_fee(1_000_000_000)
5655            .build();
5656
5657        for tx in [regular_older, expiring_newer] {
5658            regular_older_pool
5659                .add_transaction(
5660                    Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
5661                    0,
5662                    TempoHardfork::T1,
5663                )
5664                .unwrap();
5665        }
5666
5667        assert_eq!(
5668            regular_older_pool
5669                .best_transactions()
5670                .next()
5671                .unwrap()
5672                .hash(),
5673            &regular_older_hash
5674        );
5675    }
5676
5677    // ============================================
5678    // on_state_updates tests
5679    // ============================================
5680
5681    #[test]
5682    fn on_state_updates_clears_scratch_buffers_without_nonce_state() {
5683        let mut pool = AA2dPool::default();
5684        pool.state_update_nonce_changes
5685            .insert(AASequenceId::new(Address::random(), U256::from(1)), 1);
5686        pool.state_update_included_expiring_nonce_hashes
5687            .push(B256::random());
5688
5689        let state = AddressMap::default();
5690        let (promoted, mined) = pool.on_state_updates(&state);
5691
5692        assert!(promoted.is_empty());
5693        assert!(mined.is_empty());
5694        assert!(pool.state_update_nonce_changes.is_empty());
5695        assert!(pool.state_update_included_expiring_nonce_hashes.is_empty());
5696    }
5697
5698    #[test]
5699    fn test_on_state_updates_with_nonce_precompile_slot() {
5700        use revm::database::{AccountStatus, BundleAccount, states::StorageSlot};
5701
5702        let mut pool = AA2dPool::default();
5703        let sender = Address::random();
5704        let nonce_key = U256::from(1);
5705
5706        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
5707        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
5708        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
5709        let nonce_slot = tx0
5710            .nonce_key_slot()
5711            .expect("2D nonce tx should have nonce key slot");
5712
5713        pool.add_transaction(
5714            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
5715            0,
5716            TempoHardfork::T1,
5717        )
5718        .unwrap();
5719        pool.add_transaction(
5720            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
5721            0,
5722            TempoHardfork::T1,
5723        )
5724        .unwrap();
5725        pool.add_transaction(
5726            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
5727            0,
5728            TempoHardfork::T1,
5729        )
5730        .unwrap();
5731
5732        let (pending, queued) = pool.pending_and_queued_txn_count();
5733        assert_eq!(pending, 3);
5734        assert_eq!(queued, 0);
5735
5736        let mut storage = HashMap::default();
5737        storage.insert(
5738            nonce_slot,
5739            StorageSlot::new_changed(U256::ZERO, U256::from(2u64)),
5740        );
5741        let mut state = AddressMap::default();
5742        state.insert(
5743            NONCE_PRECOMPILE_ADDRESS,
5744            BundleAccount::new(None, None, storage, AccountStatus::Changed),
5745        );
5746
5747        let (promoted, mined) = pool.on_state_updates(&state);
5748
5749        assert!(promoted.is_empty(), "tx2 was already pending");
5750        assert_eq!(mined.len(), 2, "tx0 and tx1 should be mined");
5751
5752        let (pending, queued) = pool.pending_and_queued_txn_count();
5753        assert_eq!(pending, 1, "Only tx2 should remain pending");
5754        assert_eq!(queued, 0);
5755
5756        pool.assert_invariants();
5757        assert!(pool.state_update_nonce_changes.is_empty());
5758        assert!(pool.state_update_included_expiring_nonce_hashes.is_empty());
5759    }
5760
5761    #[test]
5762    fn test_on_state_updates_creates_gap_demotion() {
5763        use revm::database::{AccountStatus, BundleAccount, states::StorageSlot};
5764
5765        let mut pool = AA2dPool::default();
5766        let sender = Address::random();
5767        let nonce_key = U256::from(1);
5768
5769        let tx0 = TxBuilder::aa(sender).nonce_key(nonce_key).build();
5770        let tx1 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(1).build();
5771        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
5772        let nonce_slot = tx0
5773            .nonce_key_slot()
5774            .expect("2D nonce tx should have nonce key slot");
5775
5776        pool.add_transaction(
5777            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
5778            0,
5779            TempoHardfork::T1,
5780        )
5781        .unwrap();
5782        pool.add_transaction(
5783            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
5784            0,
5785            TempoHardfork::T1,
5786        )
5787        .unwrap();
5788        pool.add_transaction(
5789            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
5790            0,
5791            TempoHardfork::T1,
5792        )
5793        .unwrap();
5794
5795        let (pending, queued) = pool.pending_and_queued_txn_count();
5796        assert_eq!(pending, 2);
5797        assert_eq!(queued, 1);
5798
5799        let mut storage = HashMap::default();
5800        storage.insert(
5801            nonce_slot,
5802            StorageSlot::new_changed(U256::ZERO, U256::from(2u64)),
5803        );
5804        let mut state = AddressMap::default();
5805        state.insert(
5806            NONCE_PRECOMPILE_ADDRESS,
5807            BundleAccount::new(None, None, storage, AccountStatus::Changed),
5808        );
5809
5810        let (promoted, mined) = pool.on_state_updates(&state);
5811
5812        assert_eq!(mined.len(), 2, "tx0 and tx1 should be mined");
5813        assert!(promoted.is_empty());
5814
5815        let (pending, queued) = pool.pending_and_queued_txn_count();
5816        assert_eq!(pending, 0, "tx3 should still be queued (gap at nonce 2)");
5817        assert_eq!(queued, 1);
5818
5819        pool.assert_invariants();
5820    }
5821
5822    #[test]
5823    fn test_on_nonce_changes_promotes_queued_transactions() {
5824        let mut pool = AA2dPool::default();
5825        let sender = Address::random();
5826        let nonce_key = U256::ZERO;
5827        let seq_id = AASequenceId::new(sender, nonce_key);
5828
5829        let tx2 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(2).build();
5830        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
5831
5832        pool.add_transaction(
5833            Arc::new(wrap_valid_tx(tx2.clone(), TransactionOrigin::Local)),
5834            0,
5835            TempoHardfork::T1,
5836        )
5837        .unwrap();
5838        pool.add_transaction(
5839            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
5840            0,
5841            TempoHardfork::T1,
5842        )
5843        .unwrap();
5844
5845        let (pending, queued) = pool.pending_and_queued_txn_count();
5846        assert_eq!(pending, 0);
5847        assert_eq!(queued, 2);
5848
5849        let mut changes = HashMap::default();
5850        changes.insert(seq_id, 2u64);
5851
5852        let (promoted, mined) = pool.on_nonce_changes(changes);
5853
5854        assert!(
5855            mined.is_empty(),
5856            "No transactions to mine (on-chain nonce jumped)"
5857        );
5858        assert_eq!(promoted.len(), 2, "tx2 and tx3 should be promoted");
5859        assert!(promoted.iter().any(|t| t.hash() == tx2.hash()));
5860
5861        let (pending, queued) = pool.pending_and_queued_txn_count();
5862        assert_eq!(pending, 2);
5863        assert_eq!(queued, 0);
5864
5865        pool.assert_invariants();
5866    }
5867
5868    // ============================================
5869    // Interleaved inserts across sequence IDs
5870    // ============================================
5871
5872    #[test]
5873    fn test_interleaved_inserts_multiple_nonce_keys() {
5874        let mut pool = AA2dPool::default();
5875        let sender = Address::random();
5876
5877        let key_a = U256::ZERO;
5878        let key_b = U256::from(1);
5879
5880        let tx_a0 = TxBuilder::aa(sender).nonce_key(key_a).build();
5881        let tx_b0 = TxBuilder::aa(sender).nonce_key(key_b).build();
5882        let tx_a1 = TxBuilder::aa(sender).nonce_key(key_a).nonce(1).build();
5883        let tx_b2 = TxBuilder::aa(sender).nonce_key(key_b).nonce(2).build();
5884        let tx_b1 = TxBuilder::aa(sender).nonce_key(key_b).nonce(1).build();
5885
5886        pool.add_transaction(
5887            Arc::new(wrap_valid_tx(tx_a0, TransactionOrigin::Local)),
5888            0,
5889            TempoHardfork::T1,
5890        )
5891        .unwrap();
5892        pool.add_transaction(
5893            Arc::new(wrap_valid_tx(tx_b0, TransactionOrigin::Local)),
5894            0,
5895            TempoHardfork::T1,
5896        )
5897        .unwrap();
5898        pool.add_transaction(
5899            Arc::new(wrap_valid_tx(tx_a1, TransactionOrigin::Local)),
5900            0,
5901            TempoHardfork::T1,
5902        )
5903        .unwrap();
5904        pool.add_transaction(
5905            Arc::new(wrap_valid_tx(tx_b2, TransactionOrigin::Local)),
5906            0,
5907            TempoHardfork::T1,
5908        )
5909        .unwrap();
5910        pool.add_transaction(
5911            Arc::new(wrap_valid_tx(tx_b1, TransactionOrigin::Local)),
5912            0,
5913            TempoHardfork::T1,
5914        )
5915        .unwrap();
5916
5917        let (pending, queued) = pool.pending_and_queued_txn_count();
5918        assert_eq!(pending, 5, "All transactions should be pending");
5919        assert_eq!(queued, 0);
5920
5921        assert_eq!(
5922            pool.independent_transactions.len(),
5923            2,
5924            "Two nonce keys = two independent txs"
5925        );
5926
5927        pool.assert_invariants();
5928    }
5929
5930    #[test]
5931    fn test_same_sender_different_nonce_keys_independent() {
5932        let mut pool = AA2dPool::default();
5933        let sender = Address::random();
5934
5935        let key_a = U256::from(100);
5936        let key_b = U256::from(200);
5937
5938        let tx_a5 = TxBuilder::aa(sender).nonce_key(key_a).nonce(5).build();
5939        let tx_b0 = TxBuilder::aa(sender).nonce_key(key_b).build();
5940
5941        pool.add_transaction(
5942            Arc::new(wrap_valid_tx(tx_a5, TransactionOrigin::Local)),
5943            5,
5944            TempoHardfork::T1,
5945        )
5946        .unwrap();
5947        pool.add_transaction(
5948            Arc::new(wrap_valid_tx(tx_b0, TransactionOrigin::Local)),
5949            0,
5950            TempoHardfork::T1,
5951        )
5952        .unwrap();
5953
5954        let (pending, queued) = pool.pending_and_queued_txn_count();
5955        assert_eq!(pending, 2);
5956        assert_eq!(queued, 0);
5957
5958        assert_eq!(pool.independent_transactions.len(), 2);
5959
5960        pool.assert_invariants();
5961    }
5962
5963    /// Test reorg handling when on-chain nonce decreases.
5964    ///
5965    /// When a reorg occurs, the canonical nonce can decrease. If no transaction
5966    /// exists at the new on-chain nonce, `independent_transactions` must be cleared.
5967    #[test_case::test_case(U256::ZERO)]
5968    #[test_case::test_case(U256::random())]
5969    fn reorg_nonce_decrease_clears_stale_independent_transaction(nonce_key: U256) {
5970        let mut pool = AA2dPool::default();
5971        let sender = Address::random();
5972        let seq_id = AASequenceId::new(sender, nonce_key);
5973
5974        // Step 1: Add txs with nonces [3, 4, 5], starting with on_chain_nonce=3
5975        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
5976        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
5977        let tx5 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(5).build();
5978        let tx5_hash = *tx5.hash();
5979
5980        pool.add_transaction(
5981            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
5982            3,
5983            TempoHardfork::T1,
5984        )
5985        .unwrap();
5986        pool.add_transaction(
5987            Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)),
5988            3,
5989            TempoHardfork::T1,
5990        )
5991        .unwrap();
5992        pool.add_transaction(
5993            Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)),
5994            3,
5995            TempoHardfork::T1,
5996        )
5997        .unwrap();
5998
5999        // Verify initial state: all 3 txs pending, tx3 is independent
6000        let (pending, queued) = pool.pending_and_queued_txn_count();
6001        assert_eq!(pending, 3, "All transactions should be pending");
6002        assert_eq!(queued, 0);
6003        assert_eq!(pool.independent_transactions.len(), 1);
6004        assert_eq!(
6005            pool.independent_transactions
6006                .get(&seq_id)
6007                .unwrap()
6008                .transaction
6009                .nonce(),
6010            3,
6011            "tx3 should be independent initially"
6012        );
6013        pool.assert_invariants();
6014
6015        // Step 2: Simulate mining of tx3 and tx4, on_chain_nonce becomes 5
6016        let mut on_chain_ids = HashMap::default();
6017        on_chain_ids.insert(seq_id, 5u64);
6018        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
6019
6020        assert_eq!(mined.len(), 2, "tx3 and tx4 should be mined");
6021        assert!(promoted.is_empty(), "No promotions expected");
6022
6023        // Now tx5 should be the only tx in pool and be independent
6024        let (pending, queued) = pool.pending_and_queued_txn_count();
6025        assert_eq!(pending, 1, "Only tx5 should remain pending");
6026        assert_eq!(queued, 0);
6027        assert_eq!(pool.independent_transactions.len(), 1);
6028        assert_eq!(
6029            pool.independent_transactions
6030                .get(&seq_id)
6031                .unwrap()
6032                .transaction
6033                .hash(),
6034            &tx5_hash,
6035            "tx5 should be independent after mining"
6036        );
6037        pool.assert_invariants();
6038
6039        // Step 3: Simulate reorg - nonce decreases back to 3
6040        let mut on_chain_ids = HashMap::default();
6041        on_chain_ids.insert(seq_id, 3u64);
6042        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
6043
6044        // No transactions should be mined (tx5.nonce=5 >= on_chain_nonce=3)
6045        assert!(mined.is_empty(), "No transactions should be mined");
6046        // No promotions expected
6047        assert!(promoted.is_empty(), "No promotions expected");
6048
6049        // tx5 should still be in the pool but is now QUEUED (gap at nonces 3, 4)
6050        let (pending, queued) = pool.pending_and_queued_txn_count();
6051        assert_eq!(pending, 0, "tx5 should not be pending (nonce gap)");
6052        assert_eq!(queued, 1, "tx5 should be queued");
6053
6054        // No tx at on_chain_nonce=3, so independent_transactions should be cleared
6055        assert!(
6056            !pool.independent_transactions.contains_key(&seq_id),
6057            "independent_transactions should not contain stale entry after reorg"
6058        );
6059
6060        pool.assert_invariants();
6061    }
6062
6063    /// Simulates the full reorg flow as handled by reth's maintain_transaction_pool:
6064    ///
6065    /// 1. Add txs [3, 4, 5] → all pending
6066    /// 2. Mine tx3 and tx4 via on_nonce_changes(nonce=5) → tx5 remains pending
6067    /// 3. Reorg reverts the block: reth re-injects orphaned tx3 and tx4 via add_transaction
6068    ///    with the correct on_chain_nonce=3 (read from the new tip's state).
6069    ///
6070    /// This verifies that add_transaction's rescan from on_chain_nonce correctly
6071    /// reclassifies all transactions as pending without needing an explicit nonce reset.
6072    #[test_case::test_case(U256::ZERO)]
6073    #[test_case::test_case(U256::random())]
6074    fn reorg_reinjection_via_add_transaction_restores_pending_state(nonce_key: U256) {
6075        let mut pool = AA2dPool::default();
6076        let sender = Address::random();
6077        let seq_id = AASequenceId::new(sender, nonce_key);
6078
6079        // Step 1: Add txs with nonces [3, 4, 5], on_chain_nonce=3
6080        let tx3 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(3).build();
6081        let tx4 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(4).build();
6082        let tx5 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(5).build();
6083        let tx3_hash = *tx3.hash();
6084        let tx4_hash = *tx4.hash();
6085        let tx5_hash = *tx5.hash();
6086
6087        pool.add_transaction(
6088            Arc::new(wrap_valid_tx(tx3.clone(), TransactionOrigin::Local)),
6089            3,
6090            TempoHardfork::T1,
6091        )
6092        .unwrap();
6093        pool.add_transaction(
6094            Arc::new(wrap_valid_tx(tx4.clone(), TransactionOrigin::Local)),
6095            3,
6096            TempoHardfork::T1,
6097        )
6098        .unwrap();
6099        pool.add_transaction(
6100            Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)),
6101            3,
6102            TempoHardfork::T1,
6103        )
6104        .unwrap();
6105
6106        let (pending, queued) = pool.pending_and_queued_txn_count();
6107        assert_eq!(pending, 3);
6108        assert_eq!(queued, 0);
6109        pool.assert_invariants();
6110
6111        // Step 2: Mine tx3 and tx4 (on_chain_nonce becomes 5)
6112        let mut nonce_changes = HashMap::default();
6113        nonce_changes.insert(seq_id, 5u64);
6114        let (_promoted, mined) = pool.on_nonce_changes(nonce_changes);
6115        assert_eq!(mined.len(), 2);
6116
6117        let (pending, queued) = pool.pending_and_queued_txn_count();
6118        assert_eq!(pending, 1, "only tx5 should remain pending");
6119        assert_eq!(queued, 0);
6120        pool.assert_invariants();
6121
6122        // Step 3: Simulate reorg — reth re-injects orphaned tx3 and tx4 via add_transaction
6123        // with the correct on_chain_nonce=3 (reverted state).
6124        // This is exactly what reth's maintain_transaction_pool does after a reorg.
6125        pool.add_transaction(
6126            Arc::new(wrap_valid_tx(tx3, TransactionOrigin::External)),
6127            3,
6128            TempoHardfork::T1,
6129        )
6130        .unwrap();
6131        pool.add_transaction(
6132            Arc::new(wrap_valid_tx(tx4, TransactionOrigin::External)),
6133            3,
6134            TempoHardfork::T1,
6135        )
6136        .unwrap();
6137
6138        // All 3 txs should be pending again — add_transaction rescans from on_chain_nonce
6139        let (pending, queued) = pool.pending_and_queued_txn_count();
6140        assert_eq!(pending, 3, "all txs should be pending after re-injection");
6141        assert_eq!(queued, 0);
6142
6143        // tx3 should be independent (at on_chain_nonce)
6144        assert_eq!(
6145            pool.independent_transactions
6146                .get(&seq_id)
6147                .unwrap()
6148                .transaction
6149                .nonce(),
6150            3,
6151        );
6152
6153        // All txs should be in the pool
6154        assert!(pool.contains(&tx3_hash));
6155        assert!(pool.contains(&tx4_hash));
6156        assert!(pool.contains(&tx5_hash));
6157
6158        pool.assert_invariants();
6159    }
6160
6161    /// Test that gap demotion marks ALL subsequent transactions as non-pending.
6162    ///
6163    /// When a transaction is removed creating a gap, all transactions after the gap
6164    /// should be marked as queued (is_pending=false), not just the first one.
6165    #[test_case::test_case(U256::ZERO)]
6166    #[test_case::test_case(U256::random())]
6167    fn gap_demotion_marks_all_subsequent_transactions_as_queued(nonce_key: U256) {
6168        let mut pool = AA2dPool::default();
6169        let sender = Address::random();
6170        let seq_id = AASequenceId::new(sender, nonce_key);
6171
6172        // Step 1: Add txs with nonces [5, 6, 7, 8], on_chain_nonce=5
6173        let tx5 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(5).build();
6174        let tx6 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(6).build();
6175        let tx7 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(7).build();
6176        let tx8 = TxBuilder::aa(sender).nonce_key(nonce_key).nonce(8).build();
6177        let tx6_hash = *tx6.hash();
6178
6179        pool.add_transaction(
6180            Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)),
6181            5,
6182            TempoHardfork::T1,
6183        )
6184        .unwrap();
6185        pool.add_transaction(
6186            Arc::new(wrap_valid_tx(tx6, TransactionOrigin::Local)),
6187            5,
6188            TempoHardfork::T1,
6189        )
6190        .unwrap();
6191        pool.add_transaction(
6192            Arc::new(wrap_valid_tx(tx7, TransactionOrigin::Local)),
6193            5,
6194            TempoHardfork::T1,
6195        )
6196        .unwrap();
6197        pool.add_transaction(
6198            Arc::new(wrap_valid_tx(tx8, TransactionOrigin::Local)),
6199            5,
6200            TempoHardfork::T1,
6201        )
6202        .unwrap();
6203
6204        // Verify initial state: all 4 txs pending
6205        let (pending, queued) = pool.pending_and_queued_txn_count();
6206        assert_eq!(pending, 4, "All transactions should be pending initially");
6207        assert_eq!(queued, 0);
6208        assert_eq!(pool.independent_transactions.len(), 1);
6209        pool.assert_invariants();
6210
6211        // Step 2: Remove tx6 to create a gap at nonce 6
6212        // Pool now has: [5, _, 7, 8] where _ is the gap
6213        let removed = pool.remove_transactions(std::iter::once(&tx6_hash));
6214        assert_eq!(removed.len(), 1, "Should remove exactly tx6");
6215
6216        // Step 3: Trigger nonce change processing to re-evaluate pending status
6217        // The on-chain nonce is still 5
6218        let mut on_chain_ids = HashMap::default();
6219        on_chain_ids.insert(seq_id, 5u64);
6220        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
6221
6222        assert!(mined.is_empty(), "No transactions should be mined");
6223        assert!(promoted.is_empty(), "No promotions expected");
6224
6225        // Step 4: Verify that tx7 AND tx8 are both queued (not pending)
6226        // BUG: Current code only marks tx7 as non-pending, tx8 incorrectly stays pending
6227        let (pending, queued) = pool.pending_and_queued_txn_count();
6228        assert_eq!(
6229            pending, 1,
6230            "Only tx5 should be pending (tx7 and tx8 are after the gap)"
6231        );
6232        assert_eq!(
6233            queued, 2,
6234            "tx7 and tx8 should both be queued due to gap at nonce 6"
6235        );
6236
6237        pool.assert_invariants();
6238    }
6239
6240    #[test]
6241    fn expiring_nonce_tx_increments_pending_count() {
6242        let mut pool = AA2dPool::default();
6243        let sender = Address::random();
6244
6245        // Create an expiring nonce transaction (nonce_key = U256::MAX)
6246        let tx = TxBuilder::aa(sender).nonce_key(U256::MAX).build();
6247        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
6248
6249        // Add the expiring nonce transaction
6250        let result = pool.add_transaction(Arc::new(valid_tx), 0, TempoHardfork::T1);
6251        assert!(result.is_ok(), "Transaction should be added successfully");
6252        assert!(
6253            matches!(result.unwrap(), AddedTransaction::Pending(_)),
6254            "Expiring nonce transaction should be pending"
6255        );
6256
6257        // Verify counts - expiring nonce txs should increment pending_count
6258        let (pending, queued) = pool.pending_and_queued_txn_count();
6259        assert_eq!(pending, 1, "Should have 1 pending transaction");
6260        assert_eq!(queued, 0, "Should have 0 queued transactions");
6261
6262        // This will fail if pending_count wasn't incremented
6263        pool.assert_invariants();
6264    }
6265
6266    #[test]
6267    fn expiring_nonce_tx_dedup_uses_expiring_nonce_hash() {
6268        let mut pool = AA2dPool::default();
6269        let sender = Address::random();
6270        let call_to = Address::random();
6271        let fee_token = Address::random();
6272        let calls = vec![Call {
6273            to: TxKind::Call(call_to),
6274            value: U256::ZERO,
6275            input: Bytes::new(),
6276        }];
6277
6278        let build_tx = |fee_payer_signature: Signature| {
6279            let tx = TempoTransaction {
6280                chain_id: 1,
6281                max_priority_fee_per_gas: 1_000_000_000,
6282                max_fee_per_gas: 2_000_000_000,
6283                gas_limit: 1_000_000,
6284                calls: calls.clone(),
6285                nonce_key: U256::MAX,
6286                nonce: 0,
6287                fee_token: Some(fee_token),
6288                fee_payer_signature: Some(fee_payer_signature),
6289                valid_after: None,
6290                valid_before: Some(core::num::NonZeroU64::new(123).unwrap()),
6291                access_list: AccessList::default(),
6292                tempo_authorization_list: Vec::new(),
6293                key_authorization: None,
6294            };
6295
6296            let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6297                Signature::test_signature(),
6298            ));
6299            let aa_signed = AASigned::new_unhashed(tx, signature);
6300            let envelope: TempoTxEnvelope = aa_signed.into();
6301            let recovered = Recovered::new_unchecked(envelope, sender);
6302            TempoPooledTransaction::new(recovered)
6303        };
6304
6305        let tx1 = build_tx(Signature::new(U256::from(1), U256::from(2), false));
6306        let tx2 = build_tx(Signature::new(U256::from(3), U256::from(4), false));
6307
6308        assert_ne!(tx1.hash(), tx2.hash(), "tx hashes must differ");
6309        let expiring_hash_1 = tx1
6310            .expiring_nonce_hash()
6311            .expect("expiring nonce tx must be AA");
6312        let expiring_hash_2 = tx2
6313            .expiring_nonce_hash()
6314            .expect("expiring nonce tx must be AA");
6315        assert_eq!(
6316            expiring_hash_1, expiring_hash_2,
6317            "expiring nonce hashes must match"
6318        );
6319
6320        let tx1_hash = *tx1.hash();
6321        pool.add_transaction(
6322            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
6323            0,
6324            TempoHardfork::T1,
6325        )
6326        .unwrap();
6327
6328        let tx2_hash = *tx2.hash();
6329        let result = pool.add_transaction(
6330            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
6331            0,
6332            TempoHardfork::T1,
6333        );
6334        assert!(result.is_err(), "Expected AlreadyImported error");
6335        let err = result.unwrap_err();
6336        assert_eq!(err.hash, tx2_hash);
6337        assert!(
6338            matches!(err.kind, PoolErrorKind::AlreadyImported),
6339            "Expected AlreadyImported, got {:?}",
6340            err.kind
6341        );
6342
6343        let (pending, queued) = pool.pending_and_queued_txn_count();
6344        assert_eq!(pending, 1, "Expected 1 pending transaction");
6345        assert_eq!(queued, 0, "Expected 0 queued transactions");
6346        assert!(pool.by_hash.contains_key(&tx1_hash));
6347        assert_eq!(pool.expiring_nonce_txs.len(), 1);
6348        assert_expiring_eviction_index_len(&pool, 1);
6349        pool.assert_invariants();
6350    }
6351
6352    /// Verifies that removing an expiring nonce tx by hash correctly cleans up
6353    /// both `expiring_nonce_txs` and `by_hash`.
6354    #[test]
6355    fn remove_included_expiring_nonce_tx_uses_correct_key() {
6356        let mut pool = AA2dPool::default();
6357        let sender = Address::random();
6358        let fee_token = Address::random();
6359        let calls = vec![Call {
6360            to: TxKind::Call(Address::random()),
6361            value: U256::ZERO,
6362            input: Bytes::new(),
6363        }];
6364
6365        let tx = TempoTransaction {
6366            chain_id: 1,
6367            max_priority_fee_per_gas: 1_000_000_000,
6368            max_fee_per_gas: 2_000_000_000,
6369            gas_limit: 1_000_000,
6370            calls,
6371            nonce_key: U256::MAX,
6372            nonce: 0,
6373            fee_token: Some(fee_token),
6374            fee_payer_signature: Some(Signature::new(U256::from(1), U256::from(2), false)),
6375            valid_before: Some(core::num::NonZeroU64::new(123).unwrap()),
6376            access_list: AccessList::default(),
6377            tempo_authorization_list: Vec::new(),
6378            key_authorization: None,
6379            valid_after: None,
6380        };
6381
6382        let signature =
6383            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
6384        let aa_signed = AASigned::new_unhashed(tx, signature);
6385        let envelope: TempoTxEnvelope = aa_signed.into();
6386        let recovered = Recovered::new_unchecked(envelope, sender);
6387        let pooled = TempoPooledTransaction::new(recovered);
6388
6389        let tx_hash = *pooled.hash();
6390        pool.add_transaction(
6391            Arc::new(wrap_valid_tx(pooled, TransactionOrigin::Local)),
6392            0,
6393            TempoHardfork::T1,
6394        )
6395        .unwrap();
6396
6397        assert_eq!(pool.expiring_nonce_txs.len(), 1);
6398        assert_expiring_eviction_index_len(&pool, 1);
6399        assert!(pool.by_hash.contains_key(&tx_hash));
6400        pool.assert_invariants();
6401
6402        // Simulate block mining: remove by tx_hash
6403        let removed = pool.remove_transactions(std::iter::once(&tx_hash));
6404        assert_eq!(removed.len(), 1, "should remove the tx by its tx_hash");
6405        assert_eq!(*removed[0].hash(), tx_hash);
6406
6407        // Both maps must be empty
6408        assert!(
6409            pool.expiring_nonce_txs.is_empty(),
6410            "expiring_nonce_txs not cleaned up"
6411        );
6412        assert!(
6413            pool.expiring_nonce_eviction_order.is_empty(),
6414            "expiring_nonce_eviction_order not cleaned up"
6415        );
6416        assert!(
6417            !pool.by_hash.contains_key(&tx_hash),
6418            "by_hash not cleaned up"
6419        );
6420
6421        let (pending, queued) = pool.pending_and_queued_txn_count();
6422        assert_eq!(pending, 0);
6423        assert_eq!(queued, 0);
6424        pool.assert_invariants();
6425    }
6426
6427    #[test]
6428    fn on_state_updates_removes_included_expiring_nonce_from_eviction_index() {
6429        use revm::database::{AccountStatus, BundleAccount, states::StorageSlot};
6430
6431        let mut pool = AA2dPool::default();
6432        let sender = Address::random();
6433
6434        let tx = TxBuilder::aa(sender)
6435            .nonce_key(U256::MAX)
6436            .valid_before(123)
6437            .max_fee(30_000_000_000)
6438            .build();
6439        let tx_hash = *tx.hash();
6440        let expiring_hash = tx
6441            .expiring_nonce_hash()
6442            .expect("expiring nonce tx must have expiring hash");
6443        let slot = tx
6444            .expiring_nonce_slot()
6445            .expect("expiring nonce tx must have storage slot");
6446
6447        pool.add_transaction(
6448            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
6449            0,
6450            TempoHardfork::T1,
6451        )
6452        .unwrap();
6453
6454        assert_expiring_eviction_index_len(&pool, 1);
6455        assert_expiring_eviction_index_contains(&pool, expiring_hash);
6456
6457        let mut storage = HashMap::default();
6458        storage.insert(
6459            slot,
6460            StorageSlot::new_changed(U256::ZERO, U256::from(123u64)),
6461        );
6462        let mut state = AddressMap::default();
6463        state.insert(
6464            NONCE_PRECOMPILE_ADDRESS,
6465            BundleAccount::new(None, None, storage, AccountStatus::Changed),
6466        );
6467
6468        let (promoted, mined) = pool.on_state_updates(&state);
6469
6470        assert!(promoted.is_empty());
6471        assert_eq!(mined.len(), 1);
6472        assert_eq!(mined[0].hash(), &tx_hash);
6473        assert!(!pool.contains(&tx_hash));
6474        assert!(pool.expiring_nonce_txs.is_empty());
6475        assert!(pool.slot_to_expiring_nonce_hash.is_empty());
6476        assert_expiring_eviction_index_len(&pool, 0);
6477        pool.assert_invariants();
6478        assert!(pool.state_update_nonce_changes.is_empty());
6479        assert!(pool.state_update_included_expiring_nonce_hashes.is_empty());
6480    }
6481
6482    /// Pool with pending limit of 2 for eviction tests.
6483    fn eviction_test_pool() -> AA2dPool {
6484        AA2dPool::new(AA2dPoolConfig {
6485            pending_limit: SubPoolLimit {
6486                max_txs: 2,
6487                max_size: usize::MAX,
6488            },
6489            queued_limit: SubPoolLimit {
6490                max_txs: 10,
6491                max_size: usize::MAX,
6492            },
6493            ..Default::default()
6494        })
6495    }
6496
6497    fn assert_expiring_eviction_index_len(pool: &AA2dPool, len: usize) {
6498        assert_eq!(pool.expiring_nonce_txs.len(), len);
6499        assert_eq!(pool.expiring_nonce_eviction_order.len(), len);
6500        pool.assert_invariants();
6501    }
6502
6503    fn assert_expiring_eviction_index_contains(pool: &AA2dPool, expiring_hash: B256) {
6504        assert!(
6505            pool.expiring_nonce_eviction_order
6506                .iter()
6507                .any(|key| key.expiring_hash() == expiring_hash),
6508            "expiring_nonce_eviction_order should contain {expiring_hash:?}"
6509        );
6510    }
6511
6512    fn assert_expiring_eviction_index_missing(pool: &AA2dPool, expiring_hash: B256) {
6513        assert!(
6514            pool.expiring_nonce_eviction_order
6515                .iter()
6516                .all(|key| key.expiring_hash() != expiring_hash),
6517            "expiring_nonce_eviction_order should not contain {expiring_hash:?}"
6518        );
6519    }
6520
6521    #[test]
6522    fn eviction_same_priority_evicts_newer() {
6523        // Direction 1: newer expiring tx evicted over older 2D txs
6524        let mut pool = eviction_test_pool();
6525        let sender = Address::random();
6526
6527        let tx1 = TxBuilder::aa(sender)
6528            .nonce_key(U256::from(1))
6529            .nonce(0)
6530            .build();
6531        let tx2 = TxBuilder::aa(sender)
6532            .nonce_key(U256::from(2))
6533            .nonce(0)
6534            .build();
6535        let tx_exp = TxBuilder::aa(sender).nonce_key(U256::MAX).build();
6536
6537        pool.add_transaction(
6538            Arc::new(wrap_valid_tx(tx1.clone(), TransactionOrigin::Local)),
6539            0,
6540            TempoHardfork::T1,
6541        )
6542        .unwrap();
6543        pool.add_transaction(
6544            Arc::new(wrap_valid_tx(tx2.clone(), TransactionOrigin::Local)),
6545            0,
6546            TempoHardfork::T1,
6547        )
6548        .unwrap();
6549        let result = pool
6550            .add_transaction(
6551                Arc::new(wrap_valid_tx(tx_exp.clone(), TransactionOrigin::Local)),
6552                0,
6553                TempoHardfork::T1,
6554            )
6555            .unwrap();
6556
6557        let AddedTransaction::Pending(pending) = result else {
6558            panic!("expected pending")
6559        };
6560        assert_eq!(pending.discarded[0].hash(), tx_exp.hash());
6561        assert!(pool.contains(tx1.hash()));
6562        assert!(pool.contains(tx2.hash()));
6563        assert!(!pool.contains(tx_exp.hash()));
6564        pool.assert_invariants();
6565
6566        // Test opposite direction where newer 2D tx evicted over older expiring tx
6567        let mut pool = eviction_test_pool();
6568        let sender = Address::random();
6569
6570        let tx_exp = TxBuilder::aa(sender).nonce_key(U256::MAX).build();
6571        let tx2 = TxBuilder::aa(sender)
6572            .nonce_key(U256::from(1))
6573            .nonce(0)
6574            .build();
6575        let tx3 = TxBuilder::aa(sender)
6576            .nonce_key(U256::from(2))
6577            .nonce(0)
6578            .build();
6579
6580        pool.add_transaction(
6581            Arc::new(wrap_valid_tx(tx_exp.clone(), TransactionOrigin::Local)),
6582            0,
6583            TempoHardfork::T1,
6584        )
6585        .unwrap();
6586        pool.add_transaction(
6587            Arc::new(wrap_valid_tx(tx2.clone(), TransactionOrigin::Local)),
6588            0,
6589            TempoHardfork::T1,
6590        )
6591        .unwrap();
6592        let result = pool
6593            .add_transaction(
6594                Arc::new(wrap_valid_tx(tx3.clone(), TransactionOrigin::Local)),
6595                0,
6596                TempoHardfork::T1,
6597            )
6598            .unwrap();
6599
6600        let AddedTransaction::Pending(pending) = result else {
6601            panic!("expected pending")
6602        };
6603        assert_eq!(pending.discarded[0].hash(), tx3.hash());
6604        assert!(pool.contains(tx_exp.hash()));
6605        assert!(pool.contains(tx2.hash()));
6606        assert!(!pool.contains(tx3.hash()));
6607        pool.assert_invariants();
6608    }
6609
6610    #[test]
6611    fn eviction_lower_priority_expiring_evicted() {
6612        let mut pool = eviction_test_pool();
6613        let sender = Address::random();
6614
6615        // Expiring nonce tx added first but with lower priority
6616        let tx_exp = TxBuilder::aa(sender)
6617            .nonce_key(U256::MAX)
6618            .max_priority_fee(100)
6619            .max_fee(200)
6620            .build();
6621        let tx2 = TxBuilder::aa(sender)
6622            .nonce_key(U256::from(1))
6623            .nonce(0)
6624            .build();
6625        let tx3 = TxBuilder::aa(sender)
6626            .nonce_key(U256::from(2))
6627            .nonce(0)
6628            .build();
6629
6630        pool.add_transaction(
6631            Arc::new(wrap_valid_tx(tx_exp.clone(), TransactionOrigin::Local)),
6632            0,
6633            TempoHardfork::T1,
6634        )
6635        .unwrap();
6636        pool.add_transaction(
6637            Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)),
6638            0,
6639            TempoHardfork::T1,
6640        )
6641        .unwrap();
6642        let result = pool
6643            .add_transaction(
6644                Arc::new(wrap_valid_tx(tx3.clone(), TransactionOrigin::Local)),
6645                0,
6646                TempoHardfork::T1,
6647            )
6648            .unwrap();
6649
6650        // Lower-priority expiring tx evicted even though it was added first
6651        let AddedTransaction::Pending(pending) = result else {
6652            panic!("expected pending")
6653        };
6654        assert_eq!(pending.discarded[0].hash(), tx_exp.hash());
6655        assert!(!pool.contains(tx_exp.hash()));
6656        assert!(pool.contains(tx3.hash()));
6657        pool.assert_invariants();
6658    }
6659
6660    #[test]
6661    fn eviction_lower_priority_2d_evicted() {
6662        let mut pool = eviction_test_pool();
6663        let sender = Address::random();
6664
6665        // 2D tx with low priority added first
6666        let tx_low = TxBuilder::aa(sender)
6667            .nonce_key(U256::from(1))
6668            .nonce(0)
6669            .max_priority_fee(100)
6670            .max_fee(200)
6671            .build();
6672        let tx_exp = TxBuilder::aa(sender).nonce_key(U256::MAX).build();
6673        let tx3 = TxBuilder::aa(sender)
6674            .nonce_key(U256::from(2))
6675            .nonce(0)
6676            .build();
6677
6678        pool.add_transaction(
6679            Arc::new(wrap_valid_tx(tx_low.clone(), TransactionOrigin::Local)),
6680            0,
6681            TempoHardfork::T1,
6682        )
6683        .unwrap();
6684        pool.add_transaction(
6685            Arc::new(wrap_valid_tx(tx_exp.clone(), TransactionOrigin::Local)),
6686            0,
6687            TempoHardfork::T1,
6688        )
6689        .unwrap();
6690        let result = pool
6691            .add_transaction(
6692                Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)),
6693                0,
6694                TempoHardfork::T1,
6695            )
6696            .unwrap();
6697
6698        // Lower-priority 2D tx evicted even though expiring nonce tx is newer
6699        let AddedTransaction::Pending(pending) = result else {
6700            panic!("expected pending")
6701        };
6702        assert_eq!(pending.discarded[0].hash(), tx_low.hash());
6703        assert!(!pool.contains(tx_low.hash()));
6704        assert!(pool.contains(tx_exp.hash()));
6705        pool.assert_invariants();
6706    }
6707
6708    #[test]
6709    fn expiring_nonce_eviction_order_evicts_lowest_priority() {
6710        let mut pool = eviction_test_pool();
6711
6712        let tx_low = TxBuilder::aa(Address::random())
6713            .nonce_key(U256::MAX)
6714            .max_priority_fee(1_000_000_000)
6715            .max_fee(30_000_000_000)
6716            .build();
6717        let tx_high = TxBuilder::aa(Address::random())
6718            .nonce_key(U256::MAX)
6719            .max_priority_fee(3_000_000_000)
6720            .max_fee(30_000_000_000)
6721            .build();
6722        let tx_mid = TxBuilder::aa(Address::random())
6723            .nonce_key(U256::MAX)
6724            .max_priority_fee(2_000_000_000)
6725            .max_fee(30_000_000_000)
6726            .build();
6727
6728        let low_expiring_hash = tx_low
6729            .expiring_nonce_hash()
6730            .expect("expiring nonce tx must have expiring hash");
6731        let mid_expiring_hash = tx_mid
6732            .expiring_nonce_hash()
6733            .expect("expiring nonce tx must have expiring hash");
6734        let high_expiring_hash = tx_high
6735            .expiring_nonce_hash()
6736            .expect("expiring nonce tx must have expiring hash");
6737
6738        pool.add_transaction(
6739            Arc::new(wrap_valid_tx(tx_low.clone(), TransactionOrigin::Local)),
6740            0,
6741            TempoHardfork::T1,
6742        )
6743        .unwrap();
6744        pool.add_transaction(
6745            Arc::new(wrap_valid_tx(tx_high.clone(), TransactionOrigin::Local)),
6746            0,
6747            TempoHardfork::T1,
6748        )
6749        .unwrap();
6750        let result = pool
6751            .add_transaction(
6752                Arc::new(wrap_valid_tx(tx_mid.clone(), TransactionOrigin::Local)),
6753                0,
6754                TempoHardfork::T1,
6755            )
6756            .unwrap();
6757
6758        let AddedTransaction::Pending(pending) = result else {
6759            panic!("expected pending")
6760        };
6761        assert_eq!(pending.discarded.len(), 1);
6762        assert_eq!(pending.discarded[0].hash(), tx_low.hash());
6763        assert!(!pool.contains(tx_low.hash()));
6764        assert!(pool.contains(tx_mid.hash()));
6765        assert!(pool.contains(tx_high.hash()));
6766        assert_expiring_eviction_index_len(&pool, 2);
6767        assert_expiring_eviction_index_missing(&pool, low_expiring_hash);
6768        assert_expiring_eviction_index_contains(&pool, mid_expiring_hash);
6769        assert_expiring_eviction_index_contains(&pool, high_expiring_hash);
6770    }
6771
6772    #[test]
6773    fn expiring_nonce_eviction_order_evicts_newer_same_priority() {
6774        let mut pool = eviction_test_pool();
6775
6776        let tx_old_1 = TxBuilder::aa(Address::random())
6777            .nonce_key(U256::MAX)
6778            .max_fee(30_000_000_000)
6779            .build();
6780        let tx_old_2 = TxBuilder::aa(Address::random())
6781            .nonce_key(U256::MAX)
6782            .max_fee(30_000_000_000)
6783            .build();
6784        let tx_new = TxBuilder::aa(Address::random())
6785            .nonce_key(U256::MAX)
6786            .max_fee(30_000_000_000)
6787            .build();
6788
6789        let new_expiring_hash = tx_new
6790            .expiring_nonce_hash()
6791            .expect("expiring nonce tx must have expiring hash");
6792
6793        pool.add_transaction(
6794            Arc::new(wrap_valid_tx(tx_old_1.clone(), TransactionOrigin::Local)),
6795            0,
6796            TempoHardfork::T1,
6797        )
6798        .unwrap();
6799        pool.add_transaction(
6800            Arc::new(wrap_valid_tx(tx_old_2.clone(), TransactionOrigin::Local)),
6801            0,
6802            TempoHardfork::T1,
6803        )
6804        .unwrap();
6805        let result = pool
6806            .add_transaction(
6807                Arc::new(wrap_valid_tx(tx_new.clone(), TransactionOrigin::Local)),
6808                0,
6809                TempoHardfork::T1,
6810            )
6811            .unwrap();
6812
6813        let AddedTransaction::Pending(pending) = result else {
6814            panic!("expected pending")
6815        };
6816        assert_eq!(pending.discarded.len(), 1);
6817        assert_eq!(pending.discarded[0].hash(), tx_new.hash());
6818        assert!(pool.contains(tx_old_1.hash()));
6819        assert!(pool.contains(tx_old_2.hash()));
6820        assert!(!pool.contains(tx_new.hash()));
6821        assert_expiring_eviction_index_len(&pool, 2);
6822        assert_expiring_eviction_index_missing(&pool, new_expiring_hash);
6823    }
6824
6825    #[test]
6826    fn expiring_nonce_tx_uses_separate_eviction_index() {
6827        let mut pool = AA2dPool::default();
6828        let sender = Address::random();
6829
6830        let tx = TxBuilder::aa(sender).nonce_key(U256::MAX).build();
6831        let expiring_hash = tx
6832            .expiring_nonce_hash()
6833            .expect("expiring nonce tx must have expiring hash");
6834
6835        pool.add_transaction(
6836            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
6837            0,
6838            TempoHardfork::T1,
6839        )
6840        .unwrap();
6841
6842        assert!(
6843            pool.by_eviction_order.is_empty(),
6844            "expiring nonce txs should not be inserted into by_eviction_order"
6845        );
6846        assert_expiring_eviction_index_len(&pool, 1);
6847        assert_expiring_eviction_index_contains(&pool, expiring_hash);
6848    }
6849
6850    #[test]
6851    fn expiring_nonce_tx_subject_to_eviction() {
6852        // Create pool with very small pending limit
6853        let config = AA2dPoolConfig {
6854            pending_limit: SubPoolLimit {
6855                max_txs: 2,
6856                max_size: usize::MAX,
6857            },
6858            queued_limit: SubPoolLimit {
6859                max_txs: 10,
6860                max_size: usize::MAX,
6861            },
6862            ..Default::default()
6863        };
6864        let mut pool = AA2dPool::new(config);
6865        let sender = Address::random();
6866
6867        // Add 3 expiring nonce transactions - should evict to maintain limit of 2
6868        for i in 0..3 {
6869            let tx = TxBuilder::aa(sender)
6870                .nonce_key(U256::MAX)
6871                .max_priority_fee(1_000_000_000 + i as u128 * 100_000_000)
6872                .max_fee(2_000_000_000 + i as u128 * 100_000_000)
6873                .build();
6874            let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
6875            let _ = pool.add_transaction(Arc::new(valid_tx), 0, TempoHardfork::T1);
6876        }
6877
6878        // Should only have 2 transactions (evicted one to maintain limit)
6879        let (pending, queued) = pool.pending_and_queued_txn_count();
6880        assert!(
6881            pending <= 2,
6882            "Should have at most 2 pending transactions due to limit, got {pending}"
6883        );
6884        assert_eq!(queued, 0, "Should have 0 queued transactions");
6885
6886        pool.assert_invariants();
6887    }
6888
6889    #[test]
6890    fn remove_expiring_nonce_tx_decrements_pending_count() {
6891        let mut pool = AA2dPool::default();
6892        let sender = Address::random();
6893
6894        // Add two expiring nonce transactions
6895        let tx1 = TxBuilder::aa(sender)
6896            .nonce_key(U256::MAX)
6897            .max_priority_fee(1_000_000_000)
6898            .max_fee(2_000_000_000)
6899            .build();
6900        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
6901        let tx1_hash = *valid_tx1.hash();
6902        let tx1_expiring_hash = valid_tx1
6903            .transaction
6904            .expiring_nonce_hash()
6905            .expect("expiring nonce tx must have expiring hash");
6906
6907        let tx2 = TxBuilder::aa(sender)
6908            .nonce_key(U256::MAX)
6909            .max_priority_fee(1_100_000_000)
6910            .max_fee(2_200_000_000)
6911            .build();
6912        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
6913
6914        pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1)
6915            .unwrap();
6916        pool.add_transaction(Arc::new(valid_tx2), 0, TempoHardfork::T1)
6917            .unwrap();
6918
6919        // Verify we have 2 pending
6920        let (pending, _) = pool.pending_and_queued_txn_count();
6921        assert_eq!(pending, 2, "Should have 2 pending transactions");
6922        assert_expiring_eviction_index_len(&pool, 2);
6923
6924        // Remove one via hash
6925        let removed = pool.remove_transactions(std::iter::once(&tx1_hash));
6926        assert_eq!(removed.len(), 1, "Should remove exactly 1 transaction");
6927
6928        // Verify pending count decremented
6929        let (pending, _) = pool.pending_and_queued_txn_count();
6930        assert_eq!(
6931            pending, 1,
6932            "Should have 1 pending transaction after removal"
6933        );
6934        assert_expiring_eviction_index_len(&pool, 1);
6935        assert_expiring_eviction_index_missing(&pool, tx1_expiring_hash);
6936
6937        // This will fail if pending_count wasn't decremented
6938        pool.assert_invariants();
6939    }
6940
6941    #[test]
6942    fn remove_expiring_nonce_tx_by_hash_updates_pending_count() {
6943        let mut pool = AA2dPool::default();
6944        let sender = Address::random();
6945
6946        let tx = TxBuilder::aa(sender)
6947            .nonce_key(U256::MAX)
6948            .max_priority_fee(1_000_000_000)
6949            .max_fee(2_000_000_000)
6950            .build();
6951        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
6952        let tx_hash = *valid_tx.hash();
6953        let expiring_hash = valid_tx
6954            .transaction
6955            .expiring_nonce_hash()
6956            .expect("expiring nonce tx must have expiring hash");
6957
6958        pool.add_transaction(Arc::new(valid_tx), 0, TempoHardfork::T1)
6959            .unwrap();
6960
6961        let (pending, _) = pool.pending_and_queued_txn_count();
6962        assert_eq!(pending, 1);
6963        assert_expiring_eviction_index_len(&pool, 1);
6964        assert_expiring_eviction_index_contains(&pool, expiring_hash);
6965
6966        // Remove via remove_transactions (uses remove_transaction_by_hash_no_demote)
6967        let removed = pool.remove_transactions(std::iter::once(&tx_hash));
6968        assert_eq!(removed.len(), 1);
6969
6970        let (pending, _) = pool.pending_and_queued_txn_count();
6971        assert_eq!(pending, 0);
6972        assert_expiring_eviction_index_len(&pool, 0);
6973        assert_expiring_eviction_index_missing(&pool, expiring_hash);
6974    }
6975
6976    #[test]
6977    fn remove_expiring_nonce_tx_by_sender_updates_pending_count() {
6978        let mut pool = AA2dPool::default();
6979        let sender = Address::random();
6980
6981        let tx1 = TxBuilder::aa(sender)
6982            .nonce_key(U256::MAX)
6983            .max_priority_fee(1_000_000_000)
6984            .max_fee(2_000_000_000)
6985            .build();
6986        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
6987
6988        let tx2 = TxBuilder::aa(sender)
6989            .nonce_key(U256::MAX)
6990            .max_priority_fee(1_100_000_000)
6991            .max_fee(2_200_000_000)
6992            .build();
6993        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
6994
6995        pool.add_transaction(Arc::new(valid_tx1), 0, TempoHardfork::T1)
6996            .unwrap();
6997        pool.add_transaction(Arc::new(valid_tx2), 0, TempoHardfork::T1)
6998            .unwrap();
6999
7000        let (pending, _) = pool.pending_and_queued_txn_count();
7001        assert_eq!(pending, 2);
7002        assert_expiring_eviction_index_len(&pool, 2);
7003
7004        // Remove via remove_transactions_by_sender
7005        let removed = pool.remove_transactions_by_sender(sender);
7006        assert_eq!(removed.len(), 2);
7007
7008        let (pending, _) = pool.pending_and_queued_txn_count();
7009        assert_eq!(pending, 0);
7010        assert_expiring_eviction_index_len(&pool, 0);
7011    }
7012
7013    #[test]
7014    fn test_rejected_2d_tx_does_not_leak_slot_entries() {
7015        let config = AA2dPoolConfig {
7016            price_bump_config: PriceBumpConfig::default(),
7017            pending_limit: SubPoolLimit {
7018                max_txs: 1000,
7019                max_size: usize::MAX,
7020            },
7021            queued_limit: SubPoolLimit {
7022                max_txs: 1000,
7023                max_size: usize::MAX,
7024            },
7025            max_txs_per_sender: 1,
7026        };
7027        let mut pool = AA2dPool::new(config);
7028        let sender = Address::random();
7029
7030        let tx0 = TxBuilder::aa(sender)
7031            .nonce_key(U256::from(1))
7032            .nonce(0)
7033            .build();
7034        pool.add_transaction(
7035            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
7036            0,
7037            TempoHardfork::T1,
7038        )
7039        .unwrap();
7040
7041        assert_eq!(pool.slot_to_seq_id.len(), 1);
7042
7043        for i in 2..12u64 {
7044            let tx = TxBuilder::aa(sender)
7045                .nonce_key(U256::from(i))
7046                .nonce(0)
7047                .build();
7048            let result = pool.add_transaction(
7049                Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
7050                0,
7051                TempoHardfork::T1,
7052            );
7053            assert!(
7054                result.is_err(),
7055                "tx with nonce_key {i} should be rejected by sender limit"
7056            );
7057        }
7058
7059        assert_eq!(
7060            pool.slot_to_seq_id.len(),
7061            1,
7062            "rejected txs with new nonce keys should not grow slot_to_seq_id"
7063        );
7064        pool.assert_invariants();
7065    }
7066
7067    #[test_case::test_case(false ; "live updates")]
7068    #[test_case::test_case(true  ; "no updates")]
7069    fn best_transactions_live_new_tx(no_updates: bool) {
7070        let mut pool = AA2dPool::default();
7071        let sender = Address::random();
7072
7073        // Add one tx before creating the iterator
7074        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).build();
7075        let tx0_hash = *tx0.hash();
7076        pool.add_transaction(
7077            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
7078            0,
7079            TempoHardfork::T1,
7080        )
7081        .unwrap();
7082
7083        let mut best = pool.best_transactions();
7084        if no_updates {
7085            best.no_updates();
7086        }
7087
7088        // Add a new tx from a different sender while iterator is active
7089        let sender2 = Address::random();
7090        let tx1 = TxBuilder::aa(sender2).nonce_key(U256::ZERO).build();
7091        let tx1_hash = *tx1.hash();
7092        pool.add_transaction(
7093            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
7094            0,
7095            TempoHardfork::T1,
7096        )
7097        .unwrap();
7098
7099        let mut yielded = HashSet::new();
7100        for tx in best {
7101            yielded.insert(*tx.hash());
7102        }
7103
7104        assert!(
7105            yielded.contains(&tx0_hash),
7106            "should always yield pre-existing tx"
7107        );
7108        assert_eq!(
7109            yielded.contains(&tx1_hash),
7110            !no_updates,
7111            "new tx should only be yielded when live updates are enabled"
7112        );
7113    }
7114
7115    #[test]
7116    fn best_transactions_live_promoted() {
7117        let mut pool = AA2dPool::default();
7118        let sender = Address::random();
7119
7120        // Insert tx with nonce=1 (queued due to gap)
7121        let tx1 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(1).build();
7122        let tx1_hash = *tx1.hash();
7123        pool.add_transaction(
7124            Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)),
7125            0,
7126            TempoHardfork::T1,
7127        )
7128        .unwrap();
7129
7130        // Create iterator — snapshot is empty (tx1 is queued)
7131        let mut best = pool.best_transactions();
7132        assert!(best.next().is_none(), "no pending txs yet");
7133
7134        // Fill the gap with nonce=0, promoting tx1
7135        let tx0 = TxBuilder::aa(sender).nonce_key(U256::ZERO).nonce(0).build();
7136        let tx0_hash = *tx0.hash();
7137        pool.add_transaction(
7138            Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)),
7139            0,
7140            TempoHardfork::T1,
7141        )
7142        .unwrap();
7143
7144        let mut yielded = HashSet::new();
7145        for tx in best {
7146            yielded.insert(*tx.hash());
7147        }
7148
7149        assert_eq!(yielded.len(), 2, "should yield both tx0 and promoted tx1");
7150        assert!(yielded.contains(&tx0_hash));
7151        assert!(yielded.contains(&tx1_hash));
7152    }
7153
7154    #[test]
7155    fn best_transactions_live_gapped_unblock_higher_fee_not_promoted() {
7156        // Scenario: tx at nonce=1 is queued (gap). A new tx arrives at nonce=0 that fills the
7157        // gap but has higher priority than the last yielded tx. The gap-filler should be stashed
7158        // (not added to `independent`) so neither nonce=0 nor nonce=1 gets yielded.
7159        let mut pool = AA2dPool::default();
7160
7161        let sender_low = Address::random();
7162        let sender_gapped = Address::random();
7163
7164        // Add a low-priority tx from sender_low so the iterator has something to yield first.
7165        // max_fee must exceed the T1 base fee (20 gwei) so that effective_tip > 0.
7166        let tx_low = TxBuilder::aa(sender_low)
7167            .nonce_key(U256::ZERO)
7168            .max_priority_fee(1_000_000_000)
7169            .max_fee(30_000_000_000)
7170            .build();
7171        pool.add_transaction(
7172            Arc::new(wrap_valid_tx(tx_low, TransactionOrigin::Local)),
7173            0,
7174            TempoHardfork::T1,
7175        )
7176        .unwrap();
7177
7178        // Add a gapped tx (nonce=1) for sender_gapped — this will be queued.
7179        let tx_n1 = TxBuilder::aa(sender_gapped)
7180            .nonce_key(U256::ZERO)
7181            .nonce(1)
7182            .max_priority_fee(2_000_000_000)
7183            .max_fee(30_000_000_000)
7184            .build();
7185        let tx_n1_hash = *tx_n1.hash();
7186        pool.add_transaction(
7187            Arc::new(wrap_valid_tx(tx_n1, TransactionOrigin::Local)),
7188            0,
7189            TempoHardfork::T1,
7190        )
7191        .unwrap();
7192
7193        // Create iterator and yield the low-priority tx to set `last_priority`.
7194        let mut best = pool.best_transactions();
7195        let first = best.next();
7196        assert!(first.is_some(), "should yield the low-priority tx");
7197
7198        // Now fill the gap with nonce=0 that has HIGHER priority than the already-yielded tx.
7199        let tx_n0 = TxBuilder::aa(sender_gapped)
7200            .nonce_key(U256::ZERO)
7201            .nonce(0)
7202            .max_priority_fee(2_000_000_000)
7203            .max_fee(30_000_000_000)
7204            .build();
7205        let tx_n0_hash = *tx_n0.hash();
7206        pool.add_transaction(
7207            Arc::new(wrap_valid_tx(tx_n0, TransactionOrigin::Local)),
7208            0,
7209            TempoHardfork::T1,
7210        )
7211        .unwrap();
7212
7213        // Neither nonce=0 nor nonce=1 should be yielded because nonce=0's priority is higher
7214        // than what was already yielded, so it gets stashed rather than added to `independent`.
7215        let remaining: Vec<_> = best.map(|tx| *tx.hash()).collect();
7216        assert!(
7217            !remaining.contains(&tx_n0_hash),
7218            "gap-filler with higher fee must not be yielded"
7219        );
7220        assert!(
7221            !remaining.contains(&tx_n1_hash),
7222            "gapped tx must not be promoted when gap-filler is stashed"
7223        );
7224    }
7225
7226    #[test]
7227    fn best_transactions_live_expiring_nonce() {
7228        let mut pool = AA2dPool::default();
7229
7230        let mut best = pool.best_transactions();
7231
7232        // Add expiring nonce tx while iterator is active
7233        let sender = Address::random();
7234        let tx = TxBuilder::aa(sender).nonce_key(U256::MAX).nonce(0).build();
7235        let tx_hash = *tx.hash();
7236        pool.add_transaction(
7237            Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)),
7238            0,
7239            TempoHardfork::T1,
7240        )
7241        .unwrap();
7242
7243        let first = best.next();
7244        assert!(first.is_some(), "should yield the expiring nonce tx");
7245        assert_eq!(*first.unwrap().hash(), tx_hash);
7246        assert!(best.next().is_none());
7247    }
7248}