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::{Address, B256, TxHash, U256, map::HashMap};
4use reth_primitives_traits::transaction::error::InvalidTransactionError;
5use reth_tracing::tracing::trace;
6use reth_transaction_pool::{
7    BestTransactions, CoinbaseTipOrdering, PoolResult, PoolTransaction, PriceBumpConfig, Priority,
8    SubPool, SubPoolLimit, TransactionOrdering, TransactionOrigin, ValidPoolTransaction,
9    error::{InvalidPoolTransactionError, PoolError, PoolErrorKind},
10    pool::{AddedPendingTransaction, AddedTransaction, QueuedReason, pending::PendingTransaction},
11};
12use revm::database::BundleAccount;
13use std::{
14    collections::{
15        BTreeMap, BTreeSet,
16        Bound::{Excluded, Unbounded},
17        HashSet,
18        btree_map::Entry,
19        hash_map,
20    },
21    sync::Arc,
22};
23use tempo_chainspec::spec::TEMPO_BASE_FEE;
24use tempo_precompiles::NONCE_PRECOMPILE_ADDRESS;
25
26type Ordering = CoinbaseTipOrdering<TempoPooledTransaction>;
27
28/// A sub-pool that keeps track of 2D nonce transactions.
29///
30/// It maintains both pending and queued transactions.
31///
32/// 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.
33///
34/// This pool relies on state changes to track the nonces.
35///
36/// # Limitations
37///
38/// * 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.
39#[derive(Debug)]
40pub struct AA2dPool {
41    /// Keeps track of transactions inserted in the pool.
42    ///
43    /// This way we can determine when transactions were submitted to the pool.
44    submission_id: u64,
45    /// independent, pending, executable transactions, one per sequence id.
46    independent_transactions: HashMap<AASequenceId, PendingTransaction<Ordering>>,
47    /// _All_ transactions that are currently inside the pool grouped by their unique identifier.
48    by_id: BTreeMap<AA2dTransactionId, AA2dInternalTransaction>,
49    /// _All_ transactions by hash.
50    by_hash: HashMap<TxHash, Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
51    /// Reverse index for the storage slot of an account's nonce
52    ///
53    /// ```solidity
54    ///  mapping(address => mapping(uint256 => uint64)) public nonces
55    /// ```
56    ///
57    /// This identifies the account and nonce key based on the slot in the `NonceManager`.
58    address_slots: HashMap<U256, (Address, U256)>,
59    /// Settings for this sub-pool.
60    config: AA2dPoolConfig,
61    /// Metrics for tracking pool statistics
62    metrics: AA2dPoolMetrics,
63}
64
65impl Default for AA2dPool {
66    fn default() -> Self {
67        Self::new(AA2dPoolConfig::default())
68    }
69}
70
71impl AA2dPool {
72    /// Creates a new instance with the givenconfig and nonce keys
73    pub fn new(config: AA2dPoolConfig) -> Self {
74        Self {
75            submission_id: 0,
76            independent_transactions: Default::default(),
77            by_id: Default::default(),
78            by_hash: Default::default(),
79            address_slots: Default::default(),
80            config,
81            metrics: AA2dPoolMetrics::default(),
82        }
83    }
84
85    /// Updates all metrics to reflect the current state of the pool
86    fn update_metrics(&self) {
87        let (pending, queued) = self.pending_and_queued_txn_count();
88        let total = self.by_id.len();
89        self.metrics.set_transaction_counts(total, pending, queued);
90    }
91
92    /// Entrypoint for adding a 2d AA transaction.
93    ///
94    /// `on_chain_nonce` is expected to be the nonce of the sender at the time of validation.
95    /// If transaction is using 2D nonces, this is expected to be the nonce corresponding
96    /// to the transaction's nonce key.
97    pub(crate) fn add_transaction(
98        &mut self,
99        transaction: Arc<ValidPoolTransaction<TempoPooledTransaction>>,
100        on_chain_nonce: u64,
101    ) -> PoolResult<AddedTransaction<TempoPooledTransaction>> {
102        debug_assert!(
103            transaction.transaction.is_aa(),
104            "only AA transactions are supported"
105        );
106        if self.contains(transaction.hash()) {
107            return Err(PoolError::new(
108                *transaction.hash(),
109                PoolErrorKind::AlreadyImported,
110            ));
111        }
112
113        let tx_id = transaction
114            .transaction
115            .aa_transaction_id()
116            .expect("Transaction added to AA2D pool must be an AA transaction");
117
118        // Cache the nonce key slot for reverse lookup, if this transaction uses 2D nonce.
119        if transaction.transaction.is_aa_2d() {
120            self.record_2d_slot(&transaction.transaction);
121        }
122
123        if transaction.nonce() < on_chain_nonce {
124            // outdated transaction
125            return Err(PoolError::new(
126                *transaction.hash(),
127                PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus(
128                    InvalidTransactionError::NonceNotConsistent {
129                        tx: transaction.nonce(),
130                        state: on_chain_nonce,
131                    },
132                )),
133            ));
134        }
135
136        // assume the transaction is not pending, will get updated later
137        let mut inserted_as_pending = false;
138        let tx = AA2dInternalTransaction {
139            inner: PendingTransaction {
140                submission_id: self.next_id(),
141                priority: CoinbaseTipOrdering::default()
142                    .priority(&transaction.transaction, TEMPO_BASE_FEE),
143                transaction: transaction.clone(),
144            },
145            is_pending: inserted_as_pending,
146        };
147
148        // try to insert the transaction
149        let replaced = match self.by_id.entry(tx_id) {
150            Entry::Occupied(mut entry) => {
151                // Ensure the replacement transaction is not underpriced
152                if entry
153                    .get()
154                    .inner
155                    .transaction
156                    .is_underpriced(&tx.inner.transaction, &self.config.price_bump_config)
157                {
158                    return Err(PoolError::new(
159                        *transaction.hash(),
160                        PoolErrorKind::ReplacementUnderpriced,
161                    ));
162                }
163
164                Some(entry.insert(tx.clone()))
165            }
166            Entry::Vacant(entry) => {
167                entry.insert(tx.clone());
168                None
169            }
170        };
171
172        // clean up replaced
173        if let Some(replaced) = &replaced {
174            // we only need to remove it from the hash list, because we already replaced it in the by id set,
175            // and if this is the independent transaction, it will be replaced by the new transaction below
176            self.by_hash.remove(replaced.inner.transaction.hash());
177        }
178
179        // insert transaction by hash
180        self.by_hash
181            .insert(*tx.inner.transaction.hash(), tx.inner.transaction.clone());
182
183        // contains transactions directly impacted by the new transaction (filled nonca gap)
184        let mut promoted = Vec::new();
185        // now we need to scan the range and mark transactions as pending, if any
186        let on_chain_id = AA2dTransactionId::new(tx_id.seq_id, on_chain_nonce);
187        // track the next nonce we expect if the transactions are gapless
188        let mut next_nonce = on_chain_id.nonce;
189
190        // scan all the transactions with the same nonce key starting with the on chain nonce
191        // to check if our new transaction was inserted as pending and perhaps promoted more transactions
192        for (existing_id, existing_tx) in self.descendant_txs_mut(&on_chain_id) {
193            if existing_id.nonce == next_nonce {
194                match existing_id.nonce.cmp(&tx_id.nonce) {
195                    std::cmp::Ordering::Less => {
196                        // unaffected by our transaction
197                    }
198                    std::cmp::Ordering::Equal => {
199                        existing_tx.is_pending = true;
200                        inserted_as_pending = true;
201                    }
202                    std::cmp::Ordering::Greater => {
203                        // if this was previously not pending we need to promote the transaction
204                        let is_promoted = !std::mem::replace(&mut existing_tx.is_pending, true);
205                        if is_promoted {
206                            promoted.push(existing_tx.inner.transaction.clone());
207                        }
208                    }
209                }
210                // continue ungapped sequence
211                next_nonce = existing_id.nonce.saturating_add(1);
212            } else {
213                // can exit early here because we hit a nonce gap
214                break;
215            }
216        }
217
218        // Record metrics
219        self.metrics.inc_inserted();
220
221        if inserted_as_pending {
222            if !promoted.is_empty() {
223                self.metrics.inc_promoted(promoted.len());
224            }
225            // if this is the next nonce in line we can mark it as independent
226            if tx_id.nonce == on_chain_nonce {
227                self.independent_transactions.insert(tx_id.seq_id, tx.inner);
228            }
229
230            return Ok(AddedTransaction::Pending(AddedPendingTransaction {
231                transaction,
232                replaced: replaced.map(|tx| tx.inner.transaction),
233                promoted,
234                discarded: self.discard(),
235            }));
236        }
237
238        Ok(AddedTransaction::Parked {
239            transaction,
240            replaced: replaced.map(|tx| tx.inner.transaction),
241            subpool: SubPool::Queued,
242            queued_reason: Some(QueuedReason::NonceGap),
243        })
244    }
245
246    /// Returns how many pending and queued transactions are in the pool.
247    pub(crate) fn pending_and_queued_txn_count(&self) -> (usize, usize) {
248        self.by_id.values().fold((0, 0), |mut acc, tx| {
249            if tx.is_pending {
250                acc.0 += 1;
251            } else {
252                acc.1 += 1;
253            }
254            acc
255        })
256    }
257
258    /// Returns all transactions that where submitted with the given [`TransactionOrigin`]
259    pub(crate) fn get_transactions_by_origin_iter(
260        &self,
261        origin: TransactionOrigin,
262    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
263        self.by_id
264            .values()
265            .filter(move |tx| tx.inner.transaction.origin == origin)
266            .map(|tx| tx.inner.transaction.clone())
267    }
268
269    /// Returns all transactions that where submitted with the given [`TransactionOrigin`]
270    pub(crate) fn get_pending_transactions_by_origin_iter(
271        &self,
272        origin: TransactionOrigin,
273    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
274        self.by_id
275            .values()
276            .filter(move |tx| tx.is_pending && tx.inner.transaction.origin == origin)
277            .map(|tx| tx.inner.transaction.clone())
278    }
279
280    /// Returns all transactions of the address
281    pub(crate) fn get_transactions_by_sender_iter(
282        &self,
283        sender: Address,
284    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
285        self.by_id
286            .values()
287            .filter(move |tx| tx.inner.transaction.sender() == sender)
288            .map(|tx| tx.inner.transaction.clone())
289    }
290
291    /// Returns an iterator over all transaction hashes in this pool
292    pub(crate) fn all_transaction_hashes_iter(&self) -> impl Iterator<Item = TxHash> {
293        self.by_hash.keys().copied()
294    }
295
296    /// Returns all transactions from that are queued.
297    pub(crate) fn queued_transactions(
298        &self,
299    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
300        self.by_id
301            .values()
302            .filter(|tx| !tx.is_pending)
303            .map(|tx| tx.inner.transaction.clone())
304    }
305
306    /// Returns all transactions that are pending.
307    pub(crate) fn pending_transactions(
308        &self,
309    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
310        self.by_id
311            .values()
312            .filter(|tx| tx.is_pending)
313            .map(|tx| tx.inner.transaction.clone())
314    }
315
316    /// Returns the best, executable transactions for this sub-pool
317    pub(crate) fn best_transactions(&self) -> BestAA2dTransactions {
318        BestAA2dTransactions {
319            independent: self.independent_transactions.values().cloned().collect(),
320            by_id: self
321                .by_id
322                .iter()
323                .filter(|(_, tx)| tx.is_pending)
324                .map(|(id, tx)| (*id, tx.inner.clone()))
325                .collect(),
326            invalid: Default::default(),
327        }
328    }
329
330    /// Returns the transaction by hash.
331    pub(crate) fn get(
332        &self,
333        tx_hash: &TxHash,
334    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
335        self.by_hash.get(tx_hash).cloned()
336    }
337
338    /// Returns the transaction by hash.
339    pub(crate) fn get_all<'a, I>(
340        &self,
341        tx_hashes: I,
342    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
343    where
344        I: Iterator<Item = &'a TxHash> + 'a,
345    {
346        let mut ret = Vec::new();
347        for tx_hash in tx_hashes {
348            if let Some(tx) = self.get(tx_hash) {
349                ret.push(tx);
350            }
351        }
352        ret
353    }
354
355    /// Returns an iterator over all the matching transactions.
356    pub(crate) fn get_all_iter<'a, I>(
357        &'a self,
358        tx_hashes: I,
359    ) -> impl Iterator<Item = &'a Arc<ValidPoolTransaction<TempoPooledTransaction>>> + 'a
360    where
361        I: IntoIterator<Item = &'a TxHash> + 'a,
362    {
363        tx_hashes
364            .into_iter()
365            .filter_map(|tx_hash| self.by_hash.get(tx_hash))
366    }
367
368    /// Returns an iterator over all senders in this pool.
369    pub(crate) fn senders_iter(&self) -> impl Iterator<Item = &Address> {
370        self.by_id
371            .values()
372            .map(|tx| tx.inner.transaction.sender_ref())
373    }
374
375    /// Returns all mutable transactions that _follow_ after the given id but have the same sender.
376    ///
377    /// NOTE: The range is _inclusive_: if the transaction that belongs to `id` it field be the
378    /// first value.
379    fn descendant_txs_mut<'a, 'b: 'a>(
380        &'a mut self,
381        id: &'b AA2dTransactionId,
382    ) -> impl Iterator<Item = (&'a AA2dTransactionId, &'a mut AA2dInternalTransaction)> + 'a {
383        self.by_id
384            .range_mut(id..)
385            .take_while(|(other, _)| id.seq_id == other.seq_id)
386    }
387
388    /// Returns all transactions that _follow_ after the given id and have the same sender.
389    ///
390    /// NOTE: The range is _exclusive_
391    fn descendant_txs_exclusive<'a, 'b: 'a>(
392        &'a self,
393        id: &'b AA2dTransactionId,
394    ) -> impl Iterator<Item = (&'a AA2dTransactionId, &'a AA2dInternalTransaction)> + 'a {
395        self.by_id
396            .range((Excluded(id), Unbounded))
397            .take_while(|(other, _)| id.seq_id == other.seq_id)
398    }
399
400    /// Removes the transaction with the given id from all sets.
401    ///
402    /// This does __not__ shift the independent transaction forward or mark descendants as pending.
403    fn remove_transaction_by_id(
404        &mut self,
405        id: &AA2dTransactionId,
406    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
407        let tx = self.by_id.remove(id)?;
408        self.remove_independent(id);
409        self.by_hash.remove(tx.inner.transaction.hash());
410        Some(tx.inner.transaction)
411    }
412
413    /// Removes the independent transaction if it matches the given id.
414    fn remove_independent(
415        &mut self,
416        id: &AA2dTransactionId,
417    ) -> Option<PendingTransaction<Ordering>> {
418        // Only remove from independent_transactions if this is the independent transaction
419        match self.independent_transactions.entry(id.seq_id) {
420            hash_map::Entry::Occupied(entry) => {
421                // we know it's the independent tx if the tracked tx has the same nonce
422                if entry.get().transaction.nonce() == id.nonce {
423                    return Some(entry.remove());
424                }
425            }
426            hash_map::Entry::Vacant(_) => {}
427        };
428        None
429    }
430
431    /// Removes the transaction by its hash from all internal sets.
432    pub(crate) fn remove_transactions<'a, I>(
433        &mut self,
434        tx_hashes: I,
435    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
436    where
437        I: Iterator<Item = &'a TxHash> + 'a,
438    {
439        let mut txs = Vec::new();
440        for tx_hash in tx_hashes {
441            if let Some(tx) = self.remove_transaction_by_hash(tx_hash) {
442                txs.push(tx);
443            }
444        }
445        txs
446    }
447
448    /// Removes the transaction by its hash from all internal sets.
449    ///
450    /// This does __not__ shift the independent transaction forward or mark descendants as pending.
451    fn remove_transaction_by_hash(
452        &mut self,
453        tx_hash: &B256,
454    ) -> Option<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
455        let tx = self.by_hash.remove(tx_hash)?;
456        let id = tx
457            .transaction
458            .aa_transaction_id()
459            .expect("is AA transaction");
460        self.by_id.remove(&id)?;
461        self.remove_independent(&id);
462        Some(tx)
463    }
464
465    /// Removes and returns all matching transactions and their dependent transactions from the
466    /// pool.
467    pub(crate) fn remove_transactions_and_descendants<'a, I>(
468        &mut self,
469        hashes: I,
470    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>
471    where
472        I: Iterator<Item = &'a TxHash> + 'a,
473    {
474        let mut removed = Vec::new();
475        for hash in hashes {
476            if let Some(tx) = self.remove_transaction_by_hash(hash) {
477                let id = tx.transaction.aa_transaction_id();
478                removed.push(tx);
479                if let Some(id) = id {
480                    self.remove_descendants(&id, &mut removed);
481                }
482            }
483        }
484        removed
485    }
486
487    /// Removes all transactions from the given sender.
488    pub(crate) fn remove_transactions_by_sender(
489        &mut self,
490        sender_id: Address,
491    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
492        let mut removed = Vec::new();
493        let txs = self
494            .get_transactions_by_sender_iter(sender_id)
495            .collect::<Vec<_>>();
496        for tx in txs {
497            if let Some(tx) = tx
498                .transaction
499                .aa_transaction_id()
500                .and_then(|id| self.remove_transaction_by_id(&id))
501            {
502                removed.push(tx);
503            }
504        }
505        removed
506    }
507
508    /// Removes _only_ the descendants of the given transaction from this pool.
509    ///
510    /// All removed transactions are added to the `removed` vec.
511    fn remove_descendants(
512        &mut self,
513        tx: &AA2dTransactionId,
514        removed: &mut Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
515    ) {
516        let mut id = *tx;
517
518        // this will essentially pop _all_ descendant transactions one by one
519        loop {
520            let descendant = self.descendant_txs_exclusive(&id).map(|(id, _)| *id).next();
521            if let Some(descendant) = descendant {
522                if let Some(tx) = self.remove_transaction_by_id(&descendant) {
523                    removed.push(tx)
524                }
525                id = descendant;
526            } else {
527                return;
528            }
529        }
530    }
531
532    /// Updates the internal state based on the state changes of the `NonceManager` [`NONCE_PRECOMPILE_ADDRESS`].
533    ///
534    /// This takes a vec of changed [`AASequenceId`] with their current on chain nonce.
535    ///
536    /// This will prune mined transactions and promote unblocked transactions if any, returns `(promoted, mined)`
537    #[allow(clippy::type_complexity)]
538    pub(crate) fn on_nonce_changes(
539        &mut self,
540        on_chain_ids: HashMap<AASequenceId, u64>,
541    ) -> (
542        Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
543        Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
544    ) {
545        trace!(target: "txpool::2d", ?on_chain_ids, "processing nonce changes");
546
547        let mut promoted = Vec::new();
548        let mut mined_ids = Vec::new();
549
550        // we assume the set of changed senders is smaller than the individual accounts
551        'changes: for (sender_id, on_chain_nonce) in on_chain_ids {
552            let mut iter = self
553                .by_id
554                .range_mut((sender_id.start_bound(), Unbounded))
555                .take_while(move |(other, _)| sender_id == other.seq_id)
556                .peekable();
557
558            let Some(mut current) = iter.next() else {
559                continue;
560            };
561
562            // track mined transactions
563            'mined: loop {
564                if current.0.nonce < on_chain_nonce {
565                    mined_ids.push(*current.0);
566                    let Some(next) = iter.next() else {
567                        continue 'changes;
568                    };
569                    current = next;
570                } else {
571                    break 'mined;
572                }
573            }
574
575            // Process remaining transactions starting from `current` (which is >= on_chain_nonce)
576            let mut next_nonce = on_chain_nonce;
577            for (existing_id, existing_tx) in std::iter::once(current).chain(iter) {
578                if existing_id.nonce == next_nonce {
579                    // Promote if transaction was previously queued (not pending)
580                    if !std::mem::replace(&mut existing_tx.is_pending, true) {
581                        promoted.push(existing_tx.inner.transaction.clone());
582                    }
583
584                    if existing_id.nonce == on_chain_nonce {
585                        // if this is the on chain nonce we can mark it as the next independent transaction
586                        self.independent_transactions
587                            .insert(existing_id.seq_id, existing_tx.inner.clone());
588                    }
589
590                    next_nonce = next_nonce.saturating_add(1);
591                } else {
592                    // Gap detected - remaining transactions should not be pending
593                    existing_tx.is_pending = false;
594                    break;
595                }
596            }
597        }
598
599        // actually remove mined transactions
600        let mut mined = Vec::with_capacity(mined_ids.len());
601        for id in mined_ids {
602            if let Some(removed) = self.remove_transaction_by_id(&id) {
603                mined.push(removed);
604            }
605        }
606
607        // Record metrics
608        if !promoted.is_empty() {
609            self.metrics.inc_promoted(promoted.len());
610        }
611        if !mined.is_empty() {
612            self.metrics.inc_removed(mined.len());
613        }
614        self.update_metrics();
615
616        (promoted, mined)
617    }
618
619    /// Removes stale transactions if the pool is above capacity.
620    fn discard(&mut self) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
621        let mut removed = Vec::new();
622
623        while self.by_id.len() > self.config.aa_2d_limit.max_txs {
624            // TODO: this needs a more sophisticated approach for now simply pop any non pending
625            let Some(id) = self.by_id.last_key_value().map(|(id, _)| *id) else {
626                return removed;
627            };
628            removed.push(self.remove_transaction_by_id(&id).unwrap());
629        }
630
631        if !removed.is_empty() {
632            self.metrics.inc_removed(removed.len());
633        }
634
635        removed
636    }
637
638    /// Returns a reference to the metrics for this pool
639    pub fn metrics(&self) -> &AA2dPoolMetrics {
640        &self.metrics
641    }
642
643    /// Returns `true` if the transaction with the given hash is already included in this pool.
644    pub(crate) fn contains(&self, tx_hash: &TxHash) -> bool {
645        self.by_hash.contains_key(tx_hash)
646    }
647
648    /// Returns hashes of transactions in the pool that can be propagated.
649    pub(crate) fn pooled_transactions_hashes_iter(&self) -> impl Iterator<Item = TxHash> {
650        self.by_hash
651            .values()
652            .filter(|tx| tx.propagate)
653            .map(|tx| *tx.hash())
654    }
655
656    /// Returns transactions in the pool that can be propagated
657    pub(crate) fn pooled_transactions_iter(
658        &self,
659    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
660        self.by_hash.values().filter(|tx| tx.propagate).cloned()
661    }
662
663    const fn next_id(&mut self) -> u64 {
664        let id = self.submission_id;
665        self.submission_id = self.submission_id.wrapping_add(1);
666        id
667    }
668
669    /// Caches the 2D nonce key slot for the given sender and nonce key.
670    fn record_2d_slot(&mut self, transaction: &TempoPooledTransaction) {
671        let address = transaction.sender();
672        let nonce_key = transaction.nonce_key().unwrap_or_default();
673        let Some(slot) = transaction.nonce_key_slot() else {
674            return;
675        };
676
677        trace!(target: "txpool::2d", ?address, ?nonce_key, "recording 2d nonce slot");
678
679        if self
680            .address_slots
681            .insert(slot, (address, nonce_key))
682            .is_none()
683        {
684            self.metrics.inc_nonce_key_count(1);
685        }
686    }
687
688    /// Processes state updates and updates internal state accordingly.
689    #[expect(clippy::type_complexity)]
690    pub(crate) fn on_state_updates(
691        &mut self,
692        state: &HashMap<Address, BundleAccount>,
693    ) -> (
694        Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
695        Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
696    ) {
697        let mut changes = HashMap::default();
698
699        for (account, state) in state {
700            if account == &NONCE_PRECOMPILE_ADDRESS {
701                // Process known 2D nonce slot changes.
702                for (slot, value) in state.storage.iter() {
703                    if let Some((address, nonce_key)) = self.address_slots.get(slot) {
704                        changes.insert(
705                            AASequenceId::new(*address, *nonce_key),
706                            value.present_value.saturating_to(),
707                        );
708                    }
709                }
710            }
711            let nonce = state
712                .account_info()
713                .map(|info| info.nonce)
714                .unwrap_or_default();
715            changes.insert(AASequenceId::new(*account, U256::ZERO), nonce);
716        }
717
718        self.on_nonce_changes(changes)
719    }
720
721    /// Asserts that all assumptions are valid.
722    #[cfg(test)]
723    pub(crate) fn assert_invariants(&self) {
724        // Basic size constraints
725        assert!(
726            self.independent_transactions.len() <= self.by_id.len(),
727            "independent_transactions.len() ({}) > by_id.len() ({})",
728            self.independent_transactions.len(),
729            self.by_id.len()
730        );
731        assert_eq!(
732            self.by_id.len(),
733            self.by_hash.len(),
734            "by_id.len() ({}) != by_hash.len() ({})",
735            self.by_id.len(),
736            self.by_hash.len()
737        );
738
739        // All independent transactions must exist in by_id
740        for (seq_id, independent_tx) in &self.independent_transactions {
741            let tx_id = independent_tx
742                .transaction
743                .transaction
744                .aa_transaction_id()
745                .expect("Independent transaction must have AA transaction ID");
746            assert!(
747                self.by_id.contains_key(&tx_id),
748                "Independent transaction {tx_id:?} not in by_id"
749            );
750            assert_eq!(
751                seq_id, &tx_id.seq_id,
752                "Independent transactions sequence ID {seq_id:?} does not match transaction sequence ID {tx_id:?}"
753            );
754
755            // Independent transactions must be pending
756            let tx_in_pool = self.by_id.get(&tx_id).unwrap();
757            assert!(
758                tx_in_pool.is_pending,
759                "Independent transaction {tx_id:?} is not pending"
760            );
761
762            // Independent transaction should match the one in by_id
763            assert_eq!(
764                independent_tx.transaction.hash(),
765                tx_in_pool.inner.transaction.hash(),
766                "Independent transaction hash mismatch for {tx_id:?}"
767            );
768        }
769
770        // Each sender should have at most one transaction in independent set
771        let mut seen_senders = std::collections::HashSet::new();
772        for id in self.independent_transactions.keys() {
773            assert!(
774                seen_senders.insert(*id),
775                "Duplicate sender {id:?} in independent transactions"
776            );
777        }
778
779        // Verify by_hash integrity
780        for (hash, tx) in &self.by_hash {
781            // Hash should match transaction hash
782            assert_eq!(
783                tx.hash(),
784                hash,
785                "Hash mismatch in by_hash: expected {:?}, got {:?}",
786                hash,
787                tx.hash()
788            );
789
790            // Transaction in by_hash should exist in by_id
791            let id = tx
792                .transaction
793                .aa_transaction_id()
794                .expect("Transaction in pool should be AA transaction");
795            assert!(
796                self.by_id.contains_key(&id),
797                "Transaction with hash {hash:?} in by_hash but not in by_id"
798            );
799
800            // The transaction in by_id should have the same hash
801            let tx_in_by_id = &self.by_id.get(&id).unwrap().inner.transaction;
802            assert_eq!(
803                tx.hash(),
804                tx_in_by_id.hash(),
805                "Transaction hash mismatch between by_hash and by_id for id {id:?}"
806            );
807        }
808
809        // Verify by_id integrity
810        for (id, tx) in &self.by_id {
811            // Transaction in by_id should exist in by_hash
812            let hash = tx.inner.transaction.hash();
813            assert!(
814                self.by_hash.contains_key(hash),
815                "Transaction with id {id:?} in by_id but not in by_hash"
816            );
817
818            // The transaction should have the correct AA ID
819            let tx_id = tx
820                .inner
821                .transaction
822                .transaction
823                .aa_transaction_id()
824                .expect("Transaction in pool should be AA transaction");
825            assert_eq!(
826                &tx_id, id,
827                "Transaction ID mismatch: expected {id:?}, got {tx_id:?}"
828            );
829
830            // If THIS transaction is the independent transaction for its sequence, it must be pending
831            if let Some(independent_tx) = self.independent_transactions.get(&id.seq_id)
832                && independent_tx.transaction.hash() == tx.inner.transaction.hash()
833            {
834                assert!(
835                    tx.is_pending,
836                    "Transaction {id:?} is in independent set but not pending"
837                );
838            }
839        }
840
841        // Verify pending/queued consistency
842        let (pending_count, queued_count) = self.pending_and_queued_txn_count();
843        assert_eq!(
844            pending_count + queued_count,
845            self.by_id.len(),
846            "Pending ({}) + queued ({}) != total transactions ({})",
847            pending_count,
848            queued_count,
849            self.by_id.len()
850        );
851    }
852}
853
854/// Settings for the [`AA2dPoolConfig`]
855#[derive(Debug, Clone, Default)]
856pub struct AA2dPoolConfig {
857    /// Price bump (in %) for the transaction pool underpriced check.
858    pub price_bump_config: PriceBumpConfig,
859    /// How many transactions
860    pub aa_2d_limit: SubPoolLimit,
861}
862
863#[derive(Debug, Clone)]
864struct AA2dInternalTransaction {
865    /// Keeps track of the transaction
866    ///
867    /// We can use [`PendingTransaction`] here because the priority remains unchanged.
868    inner: PendingTransaction<CoinbaseTipOrdering<TempoPooledTransaction>>,
869    /// Whether this transaction is pending/executable.
870    ///
871    /// If it's not pending, it is queued.
872    is_pending: bool,
873}
874
875/// A snapshot of the sub-pool containing all executable transactions.
876#[derive(Debug)]
877pub(crate) struct BestAA2dTransactions {
878    /// pending, executable transactions sorted by their priority.
879    independent: BTreeSet<PendingTransaction<Ordering>>,
880    /// _All_ transactions that are currently inside the pool grouped by their unique identifier.
881    by_id: BTreeMap<AA2dTransactionId, PendingTransaction<Ordering>>,
882
883    /// There might be the case where a yielded transactions is invalid, this will track it.
884    invalid: HashSet<AASequenceId>,
885}
886
887impl BestAA2dTransactions {
888    /// Removes the best transaction from the set
889    fn pop_best(&mut self) -> Option<(AA2dTransactionId, PendingTransaction<Ordering>)> {
890        let tx = self.independent.pop_last()?;
891        let id = tx
892            .transaction
893            .transaction
894            .aa_transaction_id()
895            .expect("Transaction in AA2D pool must be an AA transaction with valid nonce key");
896        self.by_id.remove(&id);
897        Some((id, tx))
898    }
899
900    /// Returns the next best transaction with its priority.
901    pub(crate) fn next_tx_and_priority(
902        &mut self,
903    ) -> Option<(
904        Arc<ValidPoolTransaction<TempoPooledTransaction>>,
905        Priority<u128>,
906    )> {
907        loop {
908            let (id, best) = self.pop_best()?;
909            if self.invalid.contains(&id.seq_id) {
910                continue;
911            }
912            // Advance transaction that just got unlocked, if any.
913            if let Some(unlocked) = self.by_id.get(&id.unlocks()) {
914                self.independent.insert(unlocked.clone());
915            }
916            return Some((best.transaction, best.priority));
917        }
918    }
919}
920
921impl Iterator for BestAA2dTransactions {
922    type Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>;
923
924    fn next(&mut self) -> Option<Self::Item> {
925        self.next_tx_and_priority().map(|(tx, _)| tx)
926    }
927}
928
929impl BestTransactions for BestAA2dTransactions {
930    fn mark_invalid(&mut self, transaction: &Self::Item, _kind: &InvalidPoolTransactionError) {
931        if let Some(id) = transaction.transaction.aa_transaction_id() {
932            self.invalid.insert(id.seq_id);
933        }
934    }
935
936    fn no_updates(&mut self) {}
937
938    fn set_skip_blobs(&mut self, _skip_blobs: bool) {}
939}
940
941/// Key for identifying a unique sender sequence in 2D nonce system.
942///
943/// This combines the sender address with its nonce key, which
944#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
945pub(crate) struct AASequenceId {
946    pub(crate) address: Address,
947    pub(crate) nonce_key: U256,
948}
949
950impl AASequenceId {
951    /// Creates a new instance with the address and nonce key.
952    pub(crate) const fn new(address: Address, nonce_key: U256) -> Self {
953        Self { address, nonce_key }
954    }
955
956    const fn start_bound(self) -> std::ops::Bound<AA2dTransactionId> {
957        std::ops::Bound::Included(AA2dTransactionId::new(self, 0))
958    }
959}
960
961/// Unique identifier for an AA transaction.
962///
963/// Identified by its sender, nonce key and nonce for that nonce key.
964#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
965pub(crate) struct AA2dTransactionId {
966    /// Uniquely identifies the accounts nonce key sequence
967    pub(crate) seq_id: AASequenceId,
968    /// The nonce in that sequence
969    pub(crate) nonce: u64,
970}
971
972impl AA2dTransactionId {
973    /// Creates a new identifier.
974    pub(crate) const fn new(seq_id: AASequenceId, nonce: u64) -> Self {
975        Self { seq_id, nonce }
976    }
977
978    /// Returns the next transaction in the sequence.
979    pub(crate) fn unlocks(&self) -> Self {
980        Self::new(self.seq_id, self.nonce.saturating_add(1))
981    }
982}
983
984#[cfg(test)]
985mod tests {
986    use super::*;
987    use crate::transaction::TempoPooledTransaction;
988    use alloy_consensus::Transaction;
989    use alloy_primitives::{Address, Signature, TxKind, U256};
990    use reth_primitives_traits::Recovered;
991    use reth_transaction_pool::{PoolTransaction, ValidPoolTransaction};
992    use std::{collections::HashSet, time::Instant};
993    use tempo_primitives::{
994        TempoTxEnvelope,
995        transaction::{
996            TempoTransaction,
997            tempo_transaction::Call,
998            tt_signature::{PrimitiveSignature, TempoSignature},
999            tt_signed::AASigned,
1000        },
1001    };
1002
1003    /// Helper to create a test AA transaction with default gas prices
1004    fn create_aa_tx(sender: Address, nonce_key: U256, nonce: u64) -> TempoPooledTransaction {
1005        create_aa_tx_with_gas(sender, nonce_key, nonce, 1_000_000_000, 2_000_000_000)
1006    }
1007
1008    /// Helper to create a test AA transaction with custom gas prices
1009    fn create_aa_tx_with_gas(
1010        sender: Address,
1011        nonce_key: U256,
1012        nonce: u64,
1013        max_priority_fee: u128,
1014        max_fee: u128,
1015    ) -> TempoPooledTransaction {
1016        let tx = TempoTransaction {
1017            max_priority_fee_per_gas: max_priority_fee,
1018            max_fee_per_gas: max_fee,
1019            gas_limit: 100_000,
1020            calls: vec![Call {
1021                to: TxKind::Call(Address::random()),
1022                value: U256::from(1000),
1023                input: Default::default(),
1024            }],
1025            nonce_key,
1026            nonce,
1027            fee_token: None,
1028            fee_payer_signature: None,
1029            ..Default::default()
1030        };
1031
1032        // Create a dummy signature
1033        let signature =
1034            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1035        let aa_signed = AASigned::new_unhashed(tx, signature);
1036        let envelope: TempoTxEnvelope = aa_signed.into();
1037
1038        // Recover with the sender address (in tests we skip verification)
1039        let recovered = Recovered::new_unchecked(envelope, sender);
1040        TempoPooledTransaction::new(recovered)
1041    }
1042
1043    /// Helper to wrap a transaction in ValidPoolTransaction
1044    ///
1045    /// Note: Creates a dummy SenderId for testing since the AA2dPool doesn't use it
1046    fn wrap_valid_tx(
1047        tx: TempoPooledTransaction,
1048        origin: TransactionOrigin,
1049    ) -> ValidPoolTransaction<TempoPooledTransaction> {
1050        let tx_id = reth_transaction_pool::identifier::TransactionId::new(0u64.into(), tx.nonce());
1051        ValidPoolTransaction {
1052            transaction: tx,
1053            transaction_id: tx_id,
1054            propagate: true,
1055            timestamp: Instant::now(),
1056            origin,
1057            authority_ids: None,
1058        }
1059    }
1060
1061    #[test_case::test_case(U256::ZERO)]
1062    #[test_case::test_case(U256::random())]
1063    fn insert_pending(nonce_key: U256) {
1064        let mut pool = AA2dPool::default();
1065
1066        // Set up a sender with a tracked nonce key
1067        let sender = Address::random();
1068
1069        // Create a transaction with nonce_key=1, nonce=0 (should be pending)
1070        let tx = create_aa_tx(sender, nonce_key, 0);
1071        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
1072
1073        // Add the transaction to the pool
1074        let result = pool.add_transaction(Arc::new(valid_tx), 0);
1075
1076        // Should be added as pending
1077        assert!(result.is_ok(), "Transaction should be added successfully");
1078        let added = result.unwrap();
1079        assert!(
1080            matches!(added, AddedTransaction::Pending(_)),
1081            "Transaction should be pending, got: {added:?}"
1082        );
1083
1084        // Verify pool state
1085        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1086        assert_eq!(pending_count, 1, "Should have 1 pending transaction");
1087        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
1088
1089        pool.assert_invariants();
1090    }
1091
1092    #[test_case::test_case(U256::ZERO)]
1093    #[test_case::test_case(U256::random())]
1094    fn insert_with_nonce_gap_then_fill(nonce_key: U256) {
1095        let mut pool = AA2dPool::default();
1096
1097        // Set up a sender with a tracked nonce key
1098        let sender = Address::random();
1099
1100        // Step 1: Insert transaction with nonce=1 (creates a gap, should be queued)
1101        let tx1 = create_aa_tx(sender, nonce_key, 1);
1102        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
1103        let tx1_hash = *valid_tx1.hash();
1104
1105        let result1 = pool.add_transaction(Arc::new(valid_tx1), 0);
1106
1107        // Should be queued due to nonce gap
1108        assert!(
1109            result1.is_ok(),
1110            "Transaction 1 should be added successfully"
1111        );
1112        let added1 = result1.unwrap();
1113        assert!(
1114            matches!(
1115                added1,
1116                AddedTransaction::Parked {
1117                    subpool: SubPool::Queued,
1118                    ..
1119                }
1120            ),
1121            "Transaction 1 should be queued due to nonce gap, got: {added1:?}"
1122        );
1123
1124        // Verify pool state after first insert
1125        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1126        assert_eq!(pending_count, 0, "Should have 0 pending transactions");
1127        assert_eq!(queued_count, 1, "Should have 1 queued transaction");
1128
1129        // Verify tx1 is NOT in independent set
1130        let seq_id = AASequenceId::new(sender, nonce_key);
1131        let tx1_id = AA2dTransactionId::new(seq_id, 1);
1132        assert!(
1133            !pool.independent_transactions.contains_key(&tx1_id.seq_id),
1134            "Transaction 1 should not be in independent set yet"
1135        );
1136
1137        pool.assert_invariants();
1138
1139        // Step 2: Insert transaction with nonce=0 (fills the gap)
1140        let tx0 = create_aa_tx(sender, nonce_key, 0);
1141        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
1142        let tx0_hash = *valid_tx0.hash();
1143
1144        let result0 = pool.add_transaction(Arc::new(valid_tx0), 0);
1145
1146        // Should be pending and promote tx1
1147        assert!(
1148            result0.is_ok(),
1149            "Transaction 0 should be added successfully"
1150        );
1151        let added0 = result0.unwrap();
1152
1153        // Verify it's pending and promoted tx1
1154        match added0 {
1155            AddedTransaction::Pending(ref pending) => {
1156                assert_eq!(pending.transaction.hash(), &tx0_hash, "Should be tx0");
1157                assert_eq!(
1158                    pending.promoted.len(),
1159                    1,
1160                    "Should have promoted 1 transaction"
1161                );
1162                assert_eq!(
1163                    pending.promoted[0].hash(),
1164                    &tx1_hash,
1165                    "Should have promoted tx1"
1166                );
1167            }
1168            _ => panic!("Transaction 0 should be pending, got: {added0:?}"),
1169        }
1170
1171        // Verify pool state after filling the gap
1172        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1173        assert_eq!(pending_count, 2, "Should have 2 pending transactions");
1174        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
1175
1176        // Verify both transactions are now pending
1177        let tx0_id = AA2dTransactionId::new(seq_id, 0);
1178        assert!(
1179            pool.by_id.get(&tx0_id).unwrap().is_pending,
1180            "Transaction 0 should be pending"
1181        );
1182        assert!(
1183            pool.by_id.get(&tx1_id).unwrap().is_pending,
1184            "Transaction 1 should be pending after promotion"
1185        );
1186
1187        // Verify tx0 (at on-chain nonce) is in independent set
1188        assert!(
1189            pool.independent_transactions.contains_key(&tx0_id.seq_id),
1190            "Transaction 0 should be in independent set (at on-chain nonce)"
1191        );
1192
1193        // Verify the independent transaction for this sequence is tx0, not tx1
1194        let independent_tx = pool.independent_transactions.get(&seq_id).unwrap();
1195        assert_eq!(
1196            independent_tx.transaction.hash(),
1197            &tx0_hash,
1198            "Independent transaction should be tx0, not tx1"
1199        );
1200
1201        pool.assert_invariants();
1202    }
1203
1204    #[test_case::test_case(U256::ZERO)]
1205    #[test_case::test_case(U256::random())]
1206    fn replace_pending_transaction(nonce_key: U256) {
1207        let mut pool = AA2dPool::default();
1208
1209        // Set up a sender with a tracked nonce key
1210        let sender = Address::random();
1211
1212        // Step 1: Insert initial pending transaction with lower gas price
1213        let tx_low = create_aa_tx_with_gas(sender, nonce_key, 0, 1_000_000_000, 2_000_000_000);
1214        let valid_tx_low = wrap_valid_tx(tx_low, TransactionOrigin::Local);
1215        let tx_low_hash = *valid_tx_low.hash();
1216
1217        let result_low = pool.add_transaction(Arc::new(valid_tx_low), 0);
1218
1219        // Should be pending (at on-chain nonce)
1220        assert!(
1221            result_low.is_ok(),
1222            "Initial transaction should be added successfully"
1223        );
1224        let added_low = result_low.unwrap();
1225        assert!(
1226            matches!(added_low, AddedTransaction::Pending(_)),
1227            "Initial transaction should be pending"
1228        );
1229
1230        // Verify initial state
1231        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1232        assert_eq!(pending_count, 1, "Should have 1 pending transaction");
1233        assert_eq!(queued_count, 0, "Should have 0 queued transactions");
1234
1235        // Verify tx_low is in independent set
1236        let seq_id = AASequenceId::new(sender, nonce_key);
1237        let tx_id = AA2dTransactionId::new(seq_id, 0);
1238        assert!(
1239            pool.independent_transactions.contains_key(&tx_id.seq_id),
1240            "Initial transaction should be in independent set"
1241        );
1242
1243        // Verify the transaction in independent set is tx_low
1244        let independent_tx = pool.independent_transactions.get(&tx_id.seq_id).unwrap();
1245        assert_eq!(
1246            independent_tx.transaction.hash(),
1247            &tx_low_hash,
1248            "Independent set should contain tx_low"
1249        );
1250
1251        pool.assert_invariants();
1252
1253        // Step 2: Replace with higher gas price transaction
1254        // Price bump needs to be at least 10% higher (default price bump config)
1255        let tx_high = create_aa_tx_with_gas(sender, nonce_key, 0, 1_200_000_000, 2_400_000_000);
1256        let valid_tx_high = wrap_valid_tx(tx_high, TransactionOrigin::Local);
1257        let tx_high_hash = *valid_tx_high.hash();
1258
1259        let result_high = pool.add_transaction(Arc::new(valid_tx_high), 0);
1260
1261        // Should successfully replace
1262        assert!(
1263            result_high.is_ok(),
1264            "Replacement transaction should be added successfully"
1265        );
1266        let added_high = result_high.unwrap();
1267
1268        // Verify it's pending and replaced the old transaction
1269        match added_high {
1270            AddedTransaction::Pending(ref pending) => {
1271                assert_eq!(
1272                    pending.transaction.hash(),
1273                    &tx_high_hash,
1274                    "Should be tx_high"
1275                );
1276                assert!(
1277                    pending.replaced.is_some(),
1278                    "Should have replaced a transaction"
1279                );
1280                assert_eq!(
1281                    pending.replaced.as_ref().unwrap().hash(),
1282                    &tx_low_hash,
1283                    "Should have replaced tx_low"
1284                );
1285            }
1286            _ => panic!("Replacement transaction should be pending, got: {added_high:?}"),
1287        }
1288
1289        // Verify pool state - still 1 pending, 0 queued
1290        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1291        assert_eq!(
1292            pending_count, 1,
1293            "Should still have 1 pending transaction after replacement"
1294        );
1295        assert_eq!(queued_count, 0, "Should still have 0 queued transactions");
1296
1297        // Verify old transaction is no longer in the pool
1298        assert!(
1299            !pool.contains(&tx_low_hash),
1300            "Old transaction should be removed from pool"
1301        );
1302
1303        // Verify new transaction is in the pool
1304        assert!(
1305            pool.contains(&tx_high_hash),
1306            "New transaction should be in pool"
1307        );
1308
1309        // Verify independent set is updated with new transaction
1310        assert!(
1311            pool.independent_transactions.contains_key(&tx_id.seq_id),
1312            "Transaction ID should still be in independent set"
1313        );
1314
1315        let independent_tx_after = pool.independent_transactions.get(&tx_id.seq_id).unwrap();
1316        assert_eq!(
1317            independent_tx_after.transaction.hash(),
1318            &tx_high_hash,
1319            "Independent set should now contain tx_high (not tx_low)"
1320        );
1321
1322        // Verify the transaction in by_id is the new one
1323        let tx_in_pool = pool.by_id.get(&tx_id).unwrap();
1324        assert_eq!(
1325            tx_in_pool.inner.transaction.hash(),
1326            &tx_high_hash,
1327            "Transaction in by_id should be tx_high"
1328        );
1329        assert!(tx_in_pool.is_pending, "Transaction should be pending");
1330
1331        pool.assert_invariants();
1332    }
1333
1334    #[test_case::test_case(U256::ZERO)]
1335    #[test_case::test_case(U256::random())]
1336    fn on_chain_nonce_update_with_gaps(nonce_key: U256) {
1337        let mut pool = AA2dPool::default();
1338
1339        // Set up a sender with a tracked nonce key
1340        let sender = Address::random();
1341
1342        // Insert transactions with nonces: 0, 1, 3, 4, 6
1343        // Expected initial state:
1344        // - 0, 1: pending (consecutive from on-chain nonce 0)
1345        // - 3, 4, 6: queued (gaps at nonce 2 and 5)
1346        let tx0 = create_aa_tx(sender, nonce_key, 0);
1347        let tx1 = create_aa_tx(sender, nonce_key, 1);
1348        let tx3 = create_aa_tx(sender, nonce_key, 3);
1349        let tx4 = create_aa_tx(sender, nonce_key, 4);
1350        let tx6 = create_aa_tx(sender, nonce_key, 6);
1351
1352        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
1353        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
1354        let valid_tx3 = wrap_valid_tx(tx3, TransactionOrigin::Local);
1355        let valid_tx4 = wrap_valid_tx(tx4, TransactionOrigin::Local);
1356        let valid_tx6 = wrap_valid_tx(tx6, TransactionOrigin::Local);
1357
1358        let tx0_hash = *valid_tx0.hash();
1359        let tx1_hash = *valid_tx1.hash();
1360        let tx3_hash = *valid_tx3.hash();
1361        let tx4_hash = *valid_tx4.hash();
1362        let tx6_hash = *valid_tx6.hash();
1363
1364        // Add all transactions
1365        pool.add_transaction(Arc::new(valid_tx0), 0).unwrap();
1366        pool.add_transaction(Arc::new(valid_tx1), 0).unwrap();
1367        pool.add_transaction(Arc::new(valid_tx3), 0).unwrap();
1368        pool.add_transaction(Arc::new(valid_tx4), 0).unwrap();
1369        pool.add_transaction(Arc::new(valid_tx6), 0).unwrap();
1370
1371        // Verify initial state
1372        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1373        assert_eq!(
1374            pending_count, 2,
1375            "Should have 2 pending transactions (0, 1)"
1376        );
1377        assert_eq!(
1378            queued_count, 3,
1379            "Should have 3 queued transactions (3, 4, 6)"
1380        );
1381
1382        // Verify tx0 is in independent set
1383        let seq_id = AASequenceId::new(sender, nonce_key);
1384        let tx0_id = AA2dTransactionId::new(seq_id, 0);
1385        assert!(
1386            pool.independent_transactions.contains_key(&tx0_id.seq_id),
1387            "Transaction 0 should be in independent set"
1388        );
1389
1390        pool.assert_invariants();
1391
1392        // Step 1: Simulate mining block with nonces 0 and 1
1393        // New on-chain nonce becomes 2
1394        let mut on_chain_ids = HashMap::default();
1395        on_chain_ids.insert(seq_id, 2u64);
1396
1397        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
1398
1399        // Verify mined transactions
1400        assert_eq!(mined.len(), 2, "Should have mined 2 transactions (0, 1)");
1401        let mined_hashes: HashSet<_> = mined.iter().map(|tx| tx.hash()).collect();
1402        assert!(
1403            mined_hashes.contains(&&tx0_hash),
1404            "Transaction 0 should be mined"
1405        );
1406        assert!(
1407            mined_hashes.contains(&&tx1_hash),
1408            "Transaction 1 should be mined"
1409        );
1410
1411        // No transactions should be promoted (there's a gap at nonce 2)
1412        assert_eq!(
1413            promoted.len(),
1414            0,
1415            "No transactions should be promoted (gap at nonce 2)"
1416        );
1417
1418        // Verify pool state after mining
1419        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1420        assert_eq!(
1421            pending_count, 0,
1422            "Should have 0 pending transactions (gap at nonce 2)"
1423        );
1424        assert_eq!(
1425            queued_count, 3,
1426            "Should have 3 queued transactions (3, 4, 6)"
1427        );
1428
1429        // Verify mined transactions are removed
1430        assert!(!pool.contains(&tx0_hash), "Transaction 0 should be removed");
1431        assert!(!pool.contains(&tx1_hash), "Transaction 1 should be removed");
1432
1433        // Verify remaining transactions are still in pool
1434        assert!(pool.contains(&tx3_hash), "Transaction 3 should remain");
1435        assert!(pool.contains(&tx4_hash), "Transaction 4 should remain");
1436        assert!(pool.contains(&tx6_hash), "Transaction 6 should remain");
1437
1438        // Verify all remaining transactions are queued (not pending)
1439        let tx3_id = AA2dTransactionId::new(seq_id, 3);
1440        let tx4_id = AA2dTransactionId::new(seq_id, 4);
1441        let tx6_id = AA2dTransactionId::new(seq_id, 6);
1442
1443        assert!(
1444            !pool.by_id.get(&tx3_id).unwrap().is_pending,
1445            "Transaction 3 should be queued (gap at nonce 2)"
1446        );
1447        assert!(
1448            !pool.by_id.get(&tx4_id).unwrap().is_pending,
1449            "Transaction 4 should be queued"
1450        );
1451        assert!(
1452            !pool.by_id.get(&tx6_id).unwrap().is_pending,
1453            "Transaction 6 should be queued"
1454        );
1455
1456        // Verify independent set is empty (no transaction at on-chain nonce)
1457        assert!(
1458            pool.independent_transactions.is_empty(),
1459            "Independent set should be empty (gap at on-chain nonce 2)"
1460        );
1461
1462        pool.assert_invariants();
1463
1464        // Step 2: Simulate mining block with nonce 2
1465        // New on-chain nonce becomes 3
1466        let mut on_chain_ids = HashMap::default();
1467        on_chain_ids.insert(seq_id, 3u64);
1468
1469        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
1470
1471        // No transactions should be mined (nonce 2 was never in pool)
1472        assert_eq!(
1473            mined.len(),
1474            0,
1475            "No transactions should be mined (nonce 2 was never in pool)"
1476        );
1477
1478        // Transactions 3 and 4 should be promoted
1479        assert_eq!(promoted.len(), 2, "Transactions 3 and 4 should be promoted");
1480        let promoted_hashes: HashSet<_> = promoted.iter().map(|tx| tx.hash()).collect();
1481        assert!(
1482            promoted_hashes.contains(&&tx3_hash),
1483            "Transaction 3 should be promoted"
1484        );
1485        assert!(
1486            promoted_hashes.contains(&&tx4_hash),
1487            "Transaction 4 should be promoted"
1488        );
1489
1490        // Verify pool state after second update
1491        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1492        assert_eq!(
1493            pending_count, 2,
1494            "Should have 2 pending transactions (3, 4)"
1495        );
1496        assert_eq!(queued_count, 1, "Should have 1 queued transaction (6)");
1497
1498        // Verify transactions 3 and 4 are now pending
1499        assert!(
1500            pool.by_id.get(&tx3_id).unwrap().is_pending,
1501            "Transaction 3 should be pending"
1502        );
1503        assert!(
1504            pool.by_id.get(&tx4_id).unwrap().is_pending,
1505            "Transaction 4 should be pending"
1506        );
1507
1508        // Verify transaction 6 is still queued
1509        assert!(
1510            !pool.by_id.get(&tx6_id).unwrap().is_pending,
1511            "Transaction 6 should still be queued (gap at nonce 5)"
1512        );
1513
1514        // Verify transaction 3 is the independent transaction (at on-chain nonce)
1515        assert!(
1516            pool.independent_transactions.contains_key(&tx3_id.seq_id),
1517            "Transaction 3 should be in independent set (at on-chain nonce 3)"
1518        );
1519
1520        // Verify the independent transaction is tx3 specifically, not tx4 or tx6
1521        let independent_tx = pool.independent_transactions.get(&seq_id).unwrap();
1522        assert_eq!(
1523            independent_tx.transaction.hash(),
1524            &tx3_hash,
1525            "Independent transaction should be tx3 (nonce 3), not tx4 or tx6"
1526        );
1527
1528        pool.assert_invariants();
1529    }
1530
1531    #[test_case::test_case(U256::ZERO)]
1532    #[test_case::test_case(U256::random())]
1533    fn reject_outdated_transaction(nonce_key: U256) {
1534        let mut pool = AA2dPool::default();
1535
1536        // Set up a sender with a tracked nonce key
1537        let sender = Address::random();
1538
1539        // Create a transaction with nonce 3 (outdated)
1540        let tx = create_aa_tx(sender, nonce_key, 3);
1541        let valid_tx = wrap_valid_tx(tx, TransactionOrigin::Local);
1542
1543        // Try to insert it and specify the on-chain nonce 5, making it outdated
1544        let result = pool.add_transaction(Arc::new(valid_tx), 5);
1545
1546        // Should fail with nonce error
1547        assert!(result.is_err(), "Should reject outdated transaction");
1548
1549        let err = result.unwrap_err();
1550        assert!(
1551            matches!(
1552                err.kind,
1553                PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus(
1554                    InvalidTransactionError::NonceNotConsistent { tx: 3, state: 5 }
1555                ))
1556            ),
1557            "Should fail with NonceNotConsistent error, got: {:?}",
1558            err.kind
1559        );
1560
1561        // Pool should remain empty
1562        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1563        assert_eq!(pending_count, 0, "Pool should be empty");
1564        assert_eq!(queued_count, 0, "Pool should be empty");
1565
1566        pool.assert_invariants();
1567    }
1568
1569    #[test_case::test_case(U256::ZERO)]
1570    #[test_case::test_case(U256::random())]
1571    fn replace_with_insufficient_price_bump(nonce_key: U256) {
1572        let mut pool = AA2dPool::default();
1573
1574        // Set up a sender
1575        let sender = Address::random();
1576
1577        // Insert initial transaction
1578        let tx_low = create_aa_tx_with_gas(sender, nonce_key, 0, 1_000_000_000, 2_000_000_000);
1579        let valid_tx_low = wrap_valid_tx(tx_low, TransactionOrigin::Local);
1580
1581        pool.add_transaction(Arc::new(valid_tx_low), 0).unwrap();
1582
1583        // Try to replace with only 5% price bump (default requires 10%)
1584        let tx_insufficient =
1585            create_aa_tx_with_gas(sender, nonce_key, 0, 1_050_000_000, 2_100_000_000);
1586        let valid_tx_insufficient = wrap_valid_tx(tx_insufficient, TransactionOrigin::Local);
1587
1588        let result = pool.add_transaction(Arc::new(valid_tx_insufficient), 0);
1589
1590        // Should fail with ReplacementUnderpriced
1591        assert!(
1592            result.is_err(),
1593            "Should reject replacement with insufficient price bump"
1594        );
1595        let err = result.unwrap_err();
1596        assert!(
1597            matches!(err.kind, PoolErrorKind::ReplacementUnderpriced),
1598            "Should fail with ReplacementUnderpriced, got: {:?}",
1599            err.kind
1600        );
1601
1602        pool.assert_invariants();
1603    }
1604
1605    #[test_case::test_case(U256::ZERO)]
1606    #[test_case::test_case(U256::random())]
1607    fn fill_gap_in_middle(nonce_key: U256) {
1608        let mut pool = AA2dPool::default();
1609
1610        let sender = Address::random();
1611
1612        // Insert transactions: 0, 1, 3, 4 (gap at 2)
1613        let tx0 = create_aa_tx(sender, nonce_key, 0);
1614        let tx1 = create_aa_tx(sender, nonce_key, 1);
1615        let tx3 = create_aa_tx(sender, nonce_key, 3);
1616        let tx4 = create_aa_tx(sender, nonce_key, 4);
1617
1618        pool.add_transaction(Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)), 0)
1619            .unwrap();
1620        pool.add_transaction(Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)), 0)
1621            .unwrap();
1622        pool.add_transaction(Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)), 0)
1623            .unwrap();
1624        pool.add_transaction(Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)), 0)
1625            .unwrap();
1626
1627        // Verify initial state: 0, 1 pending | 3, 4 queued
1628        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1629        assert_eq!(pending_count, 2, "Should have 2 pending (0, 1)");
1630        assert_eq!(queued_count, 2, "Should have 2 queued (3, 4)");
1631
1632        // Fill the gap with nonce 2
1633        let tx2 = create_aa_tx(sender, nonce_key, 2);
1634        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
1635
1636        let result = pool.add_transaction(Arc::new(valid_tx2), 0);
1637        assert!(result.is_ok(), "Should successfully add tx2");
1638
1639        // Verify tx3 and tx4 were promoted
1640        match result.unwrap() {
1641            AddedTransaction::Pending(ref pending) => {
1642                assert_eq!(pending.promoted.len(), 2, "Should promote tx3 and tx4");
1643            }
1644            _ => panic!("tx2 should be added as pending"),
1645        }
1646
1647        // Verify all transactions are now pending
1648        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1649        assert_eq!(pending_count, 5, "All 5 transactions should be pending");
1650        assert_eq!(queued_count, 0, "No transactions should be queued");
1651
1652        // Verify tx0 is in independent set
1653        let seq_id = AASequenceId::new(sender, nonce_key);
1654        let tx0_id = AA2dTransactionId::new(seq_id, 0);
1655        assert!(
1656            pool.independent_transactions.contains_key(&tx0_id.seq_id),
1657            "tx0 should be in independent set"
1658        );
1659
1660        pool.assert_invariants();
1661    }
1662
1663    #[test_case::test_case(U256::ZERO)]
1664    #[test_case::test_case(U256::random())]
1665    fn remove_pending_transaction(nonce_key: U256) {
1666        let mut pool = AA2dPool::default();
1667
1668        let sender = Address::random();
1669
1670        // Insert consecutive transactions: 0, 1, 2
1671        let tx0 = create_aa_tx(sender, nonce_key, 0);
1672        let tx1 = create_aa_tx(sender, nonce_key, 1);
1673        let tx2 = create_aa_tx(sender, nonce_key, 2);
1674
1675        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
1676        let valid_tx1 = wrap_valid_tx(tx1, TransactionOrigin::Local);
1677        let valid_tx2 = wrap_valid_tx(tx2, TransactionOrigin::Local);
1678
1679        let tx1_hash = *valid_tx1.hash();
1680
1681        pool.add_transaction(Arc::new(valid_tx0), 0).unwrap();
1682        pool.add_transaction(Arc::new(valid_tx1), 0).unwrap();
1683        pool.add_transaction(Arc::new(valid_tx2), 0).unwrap();
1684
1685        // All should be pending
1686        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1687        assert_eq!(pending_count, 3, "All 3 should be pending");
1688        assert_eq!(queued_count, 0, "None should be queued");
1689
1690        let seq_id = AASequenceId::new(sender, nonce_key);
1691        let tx1_id = AA2dTransactionId::new(seq_id, 1);
1692        let tx2_id = AA2dTransactionId::new(seq_id, 2);
1693
1694        // Verify tx2 is pending before removal
1695        assert!(
1696            pool.by_id.get(&tx2_id).unwrap().is_pending,
1697            "tx2 should be pending before removal"
1698        );
1699
1700        // Remove tx1 (creates a gap)
1701        let removed = pool.remove_transactions([&tx1_hash].into_iter());
1702        assert_eq!(removed.len(), 1, "Should remove tx1");
1703
1704        // Note: Current implementation doesn't automatically re-scan and update
1705        // is_pending flags after removal. This is a known limitation.
1706        // The is_pending flag for tx2 remains true even though there's now a gap.
1707        // However, tx2 won't be included in independent_transactions or best_transactions
1708        // until the gap is filled.
1709
1710        // Verify tx1 is removed from pool
1711        assert!(!pool.by_id.contains_key(&tx1_id), "tx1 should be removed");
1712        assert!(!pool.contains(&tx1_hash), "tx1 should be removed");
1713
1714        // Verify tx0 and tx2 remain
1715        assert_eq!(pool.by_id.len(), 2, "Should have 2 transactions left");
1716
1717        pool.assert_invariants();
1718    }
1719
1720    #[test_case::test_case(U256::ZERO, U256::random())]
1721    #[test_case::test_case(U256::random(), U256::ZERO)]
1722    #[test_case::test_case(U256::random(), U256::random())]
1723    fn multiple_senders_independent_set(nonce_key_a: U256, nonce_key_b: U256) {
1724        let mut pool = AA2dPool::default();
1725
1726        // Set up two senders with different nonce keys
1727        let sender_a = Address::random();
1728        let sender_b = Address::random();
1729
1730        // Insert transactions for both senders
1731        // Sender A: [0, 1]
1732        let tx_a0 = create_aa_tx(sender_a, nonce_key_a, 0);
1733        let tx_a1 = create_aa_tx(sender_a, nonce_key_a, 1);
1734
1735        // Sender B: [0, 1]
1736        let tx_b0 = create_aa_tx(sender_b, nonce_key_b, 0);
1737        let tx_b1 = create_aa_tx(sender_b, nonce_key_b, 1);
1738
1739        let valid_tx_a0 = wrap_valid_tx(tx_a0, TransactionOrigin::Local);
1740        let valid_tx_a1 = wrap_valid_tx(tx_a1, TransactionOrigin::Local);
1741        let valid_tx_b0 = wrap_valid_tx(tx_b0, TransactionOrigin::Local);
1742        let valid_tx_b1 = wrap_valid_tx(tx_b1, TransactionOrigin::Local);
1743
1744        let tx_a0_hash = *valid_tx_a0.hash();
1745
1746        pool.add_transaction(Arc::new(valid_tx_a0), 0).unwrap();
1747        pool.add_transaction(Arc::new(valid_tx_a1), 0).unwrap();
1748        pool.add_transaction(Arc::new(valid_tx_b0), 0).unwrap();
1749        pool.add_transaction(Arc::new(valid_tx_b1), 0).unwrap();
1750
1751        // Both senders' tx0 should be in independent set
1752        let sender_a_id = AASequenceId::new(sender_a, nonce_key_a);
1753        let sender_b_id = AASequenceId::new(sender_b, nonce_key_b);
1754        let tx_a0_id = AA2dTransactionId::new(sender_a_id, 0);
1755        let tx_b0_id = AA2dTransactionId::new(sender_b_id, 0);
1756
1757        assert_eq!(
1758            pool.independent_transactions.len(),
1759            2,
1760            "Should have 2 independent transactions"
1761        );
1762        assert!(
1763            pool.independent_transactions.contains_key(&tx_a0_id.seq_id),
1764            "Sender A's tx0 should be independent"
1765        );
1766        assert!(
1767            pool.independent_transactions.contains_key(&tx_b0_id.seq_id),
1768            "Sender B's tx0 should be independent"
1769        );
1770
1771        // All 4 transactions should be pending
1772        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1773        assert_eq!(pending_count, 4, "All 4 transactions should be pending");
1774        assert_eq!(queued_count, 0, "No transactions should be queued");
1775
1776        // Simulate mining sender A's tx0
1777        let mut on_chain_ids = HashMap::default();
1778        on_chain_ids.insert(sender_a_id, 1u64);
1779
1780        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
1781
1782        // Only sender A's tx0 should be mined
1783        assert_eq!(mined.len(), 1, "Only sender A's tx0 should be mined");
1784        assert_eq!(mined[0].hash(), &tx_a0_hash, "Should mine tx_a0");
1785
1786        // No transactions should be promoted (tx_a1 was already pending)
1787        assert_eq!(
1788            promoted.len(),
1789            0,
1790            "No transactions should be promoted (tx_a1 was already pending)"
1791        );
1792
1793        // Verify independent set now has A's tx1 and B's tx0
1794        let tx_a1_id = AA2dTransactionId::new(sender_a_id, 1);
1795        assert_eq!(
1796            pool.independent_transactions.len(),
1797            2,
1798            "Should still have 2 independent transactions"
1799        );
1800        assert!(
1801            pool.independent_transactions.contains_key(&tx_a1_id.seq_id),
1802            "Sender A's tx1 should now be independent"
1803        );
1804        assert!(
1805            pool.independent_transactions.contains_key(&tx_b0_id.seq_id),
1806            "Sender B's tx0 should still be independent"
1807        );
1808
1809        pool.assert_invariants();
1810    }
1811
1812    #[test_case::test_case(U256::ZERO)]
1813    #[test_case::test_case(U256::random())]
1814    fn concurrent_replacements_same_nonce(nonce_key: U256) {
1815        let mut pool = AA2dPool::default();
1816        let sender = Address::random();
1817        let seq_id = AASequenceId {
1818            address: sender,
1819            nonce_key,
1820        };
1821
1822        // Insert initial transaction at nonce 0 with gas prices 1_000_000_000, 2_000_000_000
1823        let tx0 = create_aa_tx_with_gas(sender, nonce_key, 0, 1_000_000_000, 2_000_000_000);
1824        let tx0_hash = *tx0.hash();
1825        let valid_tx0 = wrap_valid_tx(tx0, TransactionOrigin::Local);
1826        let result = pool.add_transaction(Arc::new(valid_tx0), 0);
1827        assert!(result.is_ok());
1828        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1829        assert_eq!(pending_count + queued_count, 1);
1830
1831        // Try to replace with slightly higher gas (1_050_000_000, 2_100_000_000 = ~5% bump) - should fail (< 10% bump)
1832        let tx0_replacement1 =
1833            create_aa_tx_with_gas(sender, nonce_key, 0, 1_050_000_000, 2_100_000_000);
1834        let valid_tx1 = wrap_valid_tx(tx0_replacement1, TransactionOrigin::Local);
1835        let result = pool.add_transaction(Arc::new(valid_tx1), 0);
1836        assert!(result.is_err(), "Should reject insufficient price bump");
1837        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1838        assert_eq!(pending_count + queued_count, 1);
1839        assert!(
1840            pool.contains(&tx0_hash),
1841            "Original tx should still be present"
1842        );
1843
1844        // Replace with sufficient bump (1_100_000_000, 2_200_000_000 = 10% bump)
1845        let tx0_replacement2 =
1846            create_aa_tx_with_gas(sender, nonce_key, 0, 1_100_000_000, 2_200_000_000);
1847        let tx0_replacement2_hash = *tx0_replacement2.hash();
1848        let valid_tx2 = wrap_valid_tx(tx0_replacement2, TransactionOrigin::Local);
1849        let result = pool.add_transaction(Arc::new(valid_tx2), 0);
1850        assert!(result.is_ok(), "Should accept 10% price bump");
1851        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1852        assert_eq!(pending_count + queued_count, 1, "Pool size should remain 1");
1853        assert!(!pool.contains(&tx0_hash), "Old tx should be removed");
1854        assert!(
1855            pool.contains(&tx0_replacement2_hash),
1856            "New tx should be present"
1857        );
1858
1859        // Try to replace with even higher gas (1_500_000_000, 3_000_000_000 = ~36% bump over original)
1860        let tx0_replacement3 =
1861            create_aa_tx_with_gas(sender, nonce_key, 0, 1_500_000_000, 3_000_000_000);
1862        let tx0_replacement3_hash = *tx0_replacement3.hash();
1863        let valid_tx3 = wrap_valid_tx(tx0_replacement3, TransactionOrigin::Local);
1864        let result = pool.add_transaction(Arc::new(valid_tx3), 0);
1865        assert!(result.is_ok(), "Should accept higher price bump");
1866        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1867        assert_eq!(pending_count + queued_count, 1);
1868        assert!(
1869            !pool.contains(&tx0_replacement2_hash),
1870            "Previous tx should be removed"
1871        );
1872        assert!(
1873            pool.contains(&tx0_replacement3_hash),
1874            "Highest priority tx should win"
1875        );
1876
1877        // Verify independent set has the final replacement
1878        let tx0_id = AA2dTransactionId::new(seq_id, 0);
1879        assert!(pool.independent_transactions.contains_key(&tx0_id.seq_id));
1880
1881        pool.assert_invariants();
1882    }
1883
1884    #[test_case::test_case(U256::ZERO)]
1885    #[test_case::test_case(U256::random())]
1886    fn long_gap_chain(nonce_key: U256) {
1887        let mut pool = AA2dPool::default();
1888        let sender = Address::random();
1889        let seq_id = AASequenceId {
1890            address: sender,
1891            nonce_key,
1892        };
1893
1894        // Insert transactions with large gaps: [0, 5, 10, 15]
1895        let tx0 = create_aa_tx(sender, nonce_key, 0);
1896        let tx5 = create_aa_tx(sender, nonce_key, 5);
1897        let tx10 = create_aa_tx(sender, nonce_key, 10);
1898        let tx15 = create_aa_tx(sender, nonce_key, 15);
1899
1900        pool.add_transaction(Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)), 0)
1901            .unwrap();
1902        pool.add_transaction(Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)), 0)
1903            .unwrap();
1904        pool.add_transaction(Arc::new(wrap_valid_tx(tx10, TransactionOrigin::Local)), 0)
1905            .unwrap();
1906        pool.add_transaction(Arc::new(wrap_valid_tx(tx15, TransactionOrigin::Local)), 0)
1907            .unwrap();
1908
1909        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1910        assert_eq!(pending_count + queued_count, 4);
1911
1912        // Only tx0 should be pending, rest should be queued
1913        let tx0_id = AA2dTransactionId::new(seq_id, 0);
1914        assert!(pool.by_id.get(&tx0_id).unwrap().is_pending);
1915        assert!(
1916            !pool
1917                .by_id
1918                .get(&AA2dTransactionId::new(seq_id, 5))
1919                .unwrap()
1920                .is_pending
1921        );
1922        assert!(
1923            !pool
1924                .by_id
1925                .get(&AA2dTransactionId::new(seq_id, 10))
1926                .unwrap()
1927                .is_pending
1928        );
1929        assert!(
1930            !pool
1931                .by_id
1932                .get(&AA2dTransactionId::new(seq_id, 15))
1933                .unwrap()
1934                .is_pending
1935        );
1936        assert_eq!(pool.independent_transactions.len(), 1);
1937
1938        // Fill gap [1,2,3,4]
1939        for nonce in 1..=4 {
1940            let tx = create_aa_tx(sender, nonce_key, nonce);
1941            pool.add_transaction(Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)), 0)
1942                .unwrap();
1943        }
1944
1945        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1946        assert_eq!(pending_count + queued_count, 8);
1947
1948        // Now [0,1,2,3,4,5] should be pending
1949        for nonce in 0..=5 {
1950            let id = AA2dTransactionId::new(seq_id, nonce);
1951            assert!(
1952                pool.by_id.get(&id).unwrap().is_pending,
1953                "Nonce {nonce} should be pending"
1954            );
1955        }
1956        // [10, 15] should still be queued
1957        assert!(
1958            !pool
1959                .by_id
1960                .get(&AA2dTransactionId::new(seq_id, 10))
1961                .unwrap()
1962                .is_pending
1963        );
1964        assert!(
1965            !pool
1966                .by_id
1967                .get(&AA2dTransactionId::new(seq_id, 15))
1968                .unwrap()
1969                .is_pending
1970        );
1971
1972        // Fill gap [6,7,8,9]
1973        for nonce in 6..=9 {
1974            let tx = create_aa_tx(sender, nonce_key, nonce);
1975            pool.add_transaction(Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)), 0)
1976                .unwrap();
1977        }
1978
1979        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
1980        assert_eq!(pending_count + queued_count, 12);
1981
1982        // Now [0..=10] should be pending
1983        for nonce in 0..=10 {
1984            let id = AA2dTransactionId::new(seq_id, nonce);
1985            assert!(
1986                pool.by_id.get(&id).unwrap().is_pending,
1987                "Nonce {nonce} should be pending"
1988            );
1989        }
1990        // Only [15] should be queued
1991        assert!(
1992            !pool
1993                .by_id
1994                .get(&AA2dTransactionId::new(seq_id, 15))
1995                .unwrap()
1996                .is_pending
1997        );
1998
1999        // Fill final gap [11,12,13,14]
2000        for nonce in 11..=14 {
2001            let tx = create_aa_tx(sender, nonce_key, nonce);
2002            pool.add_transaction(Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)), 0)
2003                .unwrap();
2004        }
2005
2006        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2007        assert_eq!(pending_count + queued_count, 16);
2008
2009        // All should be pending now
2010        for nonce in 0..=15 {
2011            let id = AA2dTransactionId::new(seq_id, nonce);
2012            assert!(
2013                pool.by_id.get(&id).unwrap().is_pending,
2014                "Nonce {nonce} should be pending"
2015            );
2016        }
2017
2018        pool.assert_invariants();
2019    }
2020
2021    #[test_case::test_case(U256::ZERO)]
2022    #[test_case::test_case(U256::random())]
2023    fn remove_from_middle_of_chain(nonce_key: U256) {
2024        let mut pool = AA2dPool::default();
2025        let sender = Address::random();
2026        let seq_id = AASequenceId {
2027            address: sender,
2028            nonce_key,
2029        };
2030
2031        // Insert continuous sequence [0,1,2,3,4]
2032        for nonce in 0..=4 {
2033            let tx = create_aa_tx(sender, nonce_key, nonce);
2034            pool.add_transaction(Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)), 0)
2035                .unwrap();
2036        }
2037
2038        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2039        assert_eq!(pending_count + queued_count, 5);
2040
2041        // All should be pending
2042        for nonce in 0..=4 {
2043            assert!(
2044                pool.by_id
2045                    .get(&AA2dTransactionId::new(seq_id, nonce))
2046                    .unwrap()
2047                    .is_pending
2048            );
2049        }
2050
2051        // Remove nonce 2 from the middle
2052        let tx2_id = AA2dTransactionId::new(seq_id, 2);
2053        let tx2_hash = *pool.by_id.get(&tx2_id).unwrap().inner.transaction.hash();
2054        let removed = pool.remove_transactions([&tx2_hash].into_iter());
2055        assert_eq!(removed.len(), 1, "Should remove transaction");
2056
2057        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2058        assert_eq!(pending_count + queued_count, 4);
2059
2060        // Transaction 2 should be gone
2061        assert!(!pool.by_id.contains_key(&tx2_id));
2062
2063        // Note: Current implementation doesn't automatically re-scan after removal
2064        // So we verify that the removal succeeded but don't expect automatic gap detection
2065        // Transactions [0,1,3,4] remain in their current state
2066
2067        pool.assert_invariants();
2068    }
2069
2070    #[test_case::test_case(U256::ZERO)]
2071    #[test_case::test_case(U256::random())]
2072    fn independent_set_after_multiple_promotions(nonce_key: U256) {
2073        let mut pool = AA2dPool::default();
2074        let sender = Address::random();
2075        let seq_id = AASequenceId {
2076            address: sender,
2077            nonce_key,
2078        };
2079
2080        // Start with gaps: insert [0, 2, 4]
2081        let tx0 = create_aa_tx(sender, nonce_key, 0);
2082        let tx2 = create_aa_tx(sender, nonce_key, 2);
2083        let tx4 = create_aa_tx(sender, nonce_key, 4);
2084
2085        pool.add_transaction(Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)), 0)
2086            .unwrap();
2087        pool.add_transaction(Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)), 0)
2088            .unwrap();
2089        pool.add_transaction(Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)), 0)
2090            .unwrap();
2091
2092        // Only tx0 should be in independent set
2093        assert_eq!(pool.independent_transactions.len(), 1);
2094        assert!(pool.independent_transactions.contains_key(&seq_id));
2095
2096        // Verify initial state: tx0 pending, tx2 and tx4 queued
2097        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2098        assert_eq!(pending_count, 1);
2099        assert_eq!(queued_count, 2);
2100
2101        // Fill first gap: insert [1]
2102        let tx1 = create_aa_tx(sender, nonce_key, 1);
2103        pool.add_transaction(Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)), 0)
2104            .unwrap();
2105
2106        // Now [0, 1, 2] should be pending, tx4 still queued
2107        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2108        assert_eq!(pending_count, 3);
2109        assert_eq!(queued_count, 1);
2110
2111        // Still only tx0 in independent set
2112        assert_eq!(pool.independent_transactions.len(), 1);
2113        assert!(pool.independent_transactions.contains_key(&seq_id));
2114
2115        // Fill second gap: insert [3]
2116        let tx3 = create_aa_tx(sender, nonce_key, 3);
2117        pool.add_transaction(Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)), 0)
2118            .unwrap();
2119
2120        // Now all [0,1,2,3,4] should be pending
2121        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2122        assert_eq!(pending_count, 5);
2123        assert_eq!(queued_count, 0);
2124
2125        // Simulate mining [0,1]
2126        let mut on_chain_ids = HashMap::default();
2127        on_chain_ids.insert(seq_id, 2u64);
2128        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
2129
2130        // Should have mined [0,1], no promotions (already pending)
2131        assert_eq!(mined.len(), 2);
2132        assert_eq!(promoted.len(), 0);
2133
2134        // Now tx2 should be in independent set
2135        assert_eq!(pool.independent_transactions.len(), 1);
2136        assert!(pool.independent_transactions.contains_key(&seq_id));
2137
2138        // Verify [2,3,4] remain in pool
2139        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2140        assert_eq!(pending_count + queued_count, 3);
2141
2142        pool.assert_invariants();
2143    }
2144
2145    #[test]
2146    fn stress_test_many_senders() {
2147        let mut pool = AA2dPool::default();
2148        const NUM_SENDERS: usize = 100;
2149        const TXS_PER_SENDER: u64 = 5;
2150
2151        // Create 100 senders, each with 5 transactions
2152        let mut senders = Vec::new();
2153        for i in 0..NUM_SENDERS {
2154            let sender = Address::from_word(B256::from(U256::from(i)));
2155            let nonce_key = U256::from(i);
2156            senders.push((sender, nonce_key));
2157
2158            // Insert transactions [0,1,2,3,4] for each sender
2159            for nonce in 0..TXS_PER_SENDER {
2160                let tx = create_aa_tx(sender, nonce_key, nonce);
2161                pool.add_transaction(Arc::new(wrap_valid_tx(tx, TransactionOrigin::Local)), 0)
2162                    .unwrap();
2163            }
2164        }
2165
2166        // Verify pool size
2167        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2168        assert_eq!(
2169            pending_count + queued_count,
2170            NUM_SENDERS * TXS_PER_SENDER as usize
2171        );
2172
2173        // Each sender should have all transactions pending
2174        for (sender, nonce_key) in &senders {
2175            let seq_id = AASequenceId {
2176                address: *sender,
2177                nonce_key: *nonce_key,
2178            };
2179            for nonce in 0..TXS_PER_SENDER {
2180                let id = AA2dTransactionId::new(seq_id, nonce);
2181                assert!(pool.by_id.get(&id).unwrap().is_pending);
2182            }
2183        }
2184
2185        // Independent set should have exactly NUM_SENDERS transactions (one per sender at nonce 0)
2186        assert_eq!(pool.independent_transactions.len(), NUM_SENDERS);
2187        for (sender, nonce_key) in &senders {
2188            let seq_id = AASequenceId {
2189                address: *sender,
2190                nonce_key: *nonce_key,
2191            };
2192            let tx0_id = AA2dTransactionId::new(seq_id, 0);
2193            assert!(
2194                pool.independent_transactions.contains_key(&tx0_id.seq_id),
2195                "Sender {sender:?} should have tx0 in independent set"
2196            );
2197        }
2198
2199        // Simulate mining first transaction for each sender
2200        let mut on_chain_ids = HashMap::default();
2201        for (sender, nonce_key) in &senders {
2202            let seq_id = AASequenceId {
2203                address: *sender,
2204                nonce_key: *nonce_key,
2205            };
2206            on_chain_ids.insert(seq_id, 1u64);
2207        }
2208
2209        let (promoted, mined) = pool.on_nonce_changes(on_chain_ids);
2210
2211        // Should have mined NUM_SENDERS transactions
2212        assert_eq!(mined.len(), NUM_SENDERS);
2213        // No promotions - transactions [1,2,3,4] were already pending
2214        assert_eq!(promoted.len(), 0);
2215
2216        // Pool size should be reduced
2217        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2218        assert_eq!(
2219            pending_count + queued_count,
2220            NUM_SENDERS * (TXS_PER_SENDER - 1) as usize
2221        );
2222
2223        // Independent set should still have NUM_SENDERS transactions (now at nonce 1)
2224        assert_eq!(pool.independent_transactions.len(), NUM_SENDERS);
2225        for (sender, nonce_key) in &senders {
2226            let seq_id = AASequenceId {
2227                address: *sender,
2228                nonce_key: *nonce_key,
2229            };
2230            let tx1_id = AA2dTransactionId::new(seq_id, 1);
2231            assert!(
2232                pool.independent_transactions.contains_key(&tx1_id.seq_id),
2233                "Sender {sender:?} should have tx1 in independent set"
2234            );
2235        }
2236
2237        pool.assert_invariants();
2238    }
2239
2240    #[test_case::test_case(U256::ZERO)]
2241    #[test_case::test_case(U256::random())]
2242    fn on_chain_nonce_update_to_queued_tx_with_gaps(nonce_key: U256) {
2243        let mut pool = AA2dPool::default();
2244        let sender = Address::random();
2245        let seq_id = AASequenceId {
2246            address: sender,
2247            nonce_key,
2248        };
2249
2250        // Start with gaps: insert [0, 3, 5]
2251        // This creates: tx0 (pending), tx3 (queued), tx5 (queued)
2252        let tx0 = create_aa_tx(sender, nonce_key, 0);
2253        let tx3 = create_aa_tx(sender, nonce_key, 3);
2254        let tx5 = create_aa_tx(sender, nonce_key, 5);
2255
2256        pool.add_transaction(Arc::new(wrap_valid_tx(tx0, TransactionOrigin::Local)), 0)
2257            .unwrap();
2258        pool.add_transaction(Arc::new(wrap_valid_tx(tx3, TransactionOrigin::Local)), 0)
2259            .unwrap();
2260        pool.add_transaction(Arc::new(wrap_valid_tx(tx5, TransactionOrigin::Local)), 0)
2261            .unwrap();
2262
2263        // Only tx0 should be in independent set
2264        assert_eq!(pool.independent_transactions.len(), 1);
2265        assert!(pool.independent_transactions.contains_key(&seq_id));
2266
2267        // Verify initial state: tx0 pending, tx3 and tx5 queued
2268        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2269        assert_eq!(pending_count, 1, "Only tx0 should be pending");
2270        assert_eq!(queued_count, 2, "tx3 and tx5 should be queued");
2271
2272        // Fill gaps to get [0, 1, 2, 3, 5]
2273        let tx1 = create_aa_tx(sender, nonce_key, 1);
2274        pool.add_transaction(Arc::new(wrap_valid_tx(tx1, TransactionOrigin::Local)), 0)
2275            .unwrap();
2276
2277        let tx2 = create_aa_tx(sender, nonce_key, 2);
2278        pool.add_transaction(Arc::new(wrap_valid_tx(tx2, TransactionOrigin::Local)), 0)
2279            .unwrap();
2280
2281        // Now [0,1,2,3] should be pending, tx5 still queued
2282        let (pending_count, queued_count) = pool.pending_and_queued_txn_count();
2283        assert_eq!(pending_count, 4, "Transactions [0,1,2,3] should be pending");
2284        assert_eq!(queued_count, 1, "tx5 should still be queued");
2285
2286        // Still only tx0 in independent set (at on-chain nonce 0)
2287        assert_eq!(pool.independent_transactions.len(), 1);
2288        assert!(pool.independent_transactions.contains_key(&seq_id));
2289
2290        let mut on_chain_ids = HashMap::default();
2291        on_chain_ids.insert(seq_id, 3u64);
2292        let (_promoted, mined) = pool.on_nonce_changes(on_chain_ids);
2293
2294        // Should have mined [0,1,2]
2295        assert_eq!(mined.len(), 3, "Should mine transactions [0,1,2]");
2296
2297        // tx3 was already pending, so no promotions expected
2298        // After mining, tx3 should be in independent set
2299        assert_eq!(
2300            pool.independent_transactions.len(),
2301            1,
2302            "Should have one independent transaction"
2303        );
2304        let key = AA2dTransactionId::new(seq_id, 3);
2305        assert!(
2306            pool.independent_transactions.contains_key(&key.seq_id),
2307            "tx3 should be in independent set"
2308        );
2309
2310        // Verify remaining pool state
2311        let (_pending_count, _queued_count) = pool.pending_and_queued_txn_count();
2312        // Should have tx3 (pending at on-chain nonce) and tx5 (queued due to gap at 4)
2313
2314        pool.assert_invariants();
2315
2316        // Now insert tx4 to fill the gap between tx3 and tx5
2317        // This is where the original test failure occurred
2318        let tx4 = create_aa_tx(sender, nonce_key, 4);
2319        pool.add_transaction(Arc::new(wrap_valid_tx(tx4, TransactionOrigin::Local)), 3)
2320            .unwrap();
2321
2322        // After inserting tx4, we should have [3, 4, 5] all in the pool
2323        let (_pending_count_after, _queued_count_after) = pool.pending_and_queued_txn_count();
2324        pool.assert_invariants();
2325    }
2326}