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