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