Skip to main content

tempo_transaction_pool/
best.rs

1//! An iterator over the best transactions in the tempo pool.
2
3use crate::{
4    ordering::TempoTipOrdering, transaction::TempoPooledTransaction,
5    tt_2d_pool::BestAA2dTransactions,
6};
7use alloy_primitives::{Address, U256, map::HashMap};
8use reth_evm::block::TxResult;
9use reth_primitives_traits::transaction::error::InvalidTransactionError;
10use reth_transaction_pool::{
11    BestTransactions, Priority, TransactionOrdering, ValidPoolTransaction,
12    error::InvalidPoolTransactionError,
13};
14use std::sync::Arc;
15use tempo_evm::TempoTxResult;
16use tempo_precompiles::tip20::is_tip20_prefix;
17
18pub type BestTransaction = Arc<ValidPoolTransaction<TempoPooledTransaction>>;
19type BestTransactionWithPriority = (BestTransaction, Priority<u64>);
20
21/// A best-transaction iterator that merges the protocol pool and the 2D nonces pool,
22/// always yielding the next best item from either iterator.
23pub struct MergeBestTransactions {
24    protocol_pool: Box<dyn BestTransactions<Item = BestTransaction>>,
25    aa_2d_pool: BestAA2dTransactions,
26    next_protocol_pool: Option<BestTransactionWithPriority>,
27    next_aa_2d_pool: Option<BestTransactionWithPriority>,
28    base_fee: u64,
29}
30
31impl MergeBestTransactions {
32    /// Creates a new iterator over the given iterators.
33    pub(crate) fn new(
34        protocol_pool: Box<dyn BestTransactions<Item = BestTransaction>>,
35        aa_2d_pool: BestAA2dTransactions,
36        base_fee: u64,
37    ) -> Self {
38        Self {
39            protocol_pool,
40            aa_2d_pool,
41            next_protocol_pool: None,
42            next_aa_2d_pool: None,
43            base_fee,
44        }
45    }
46}
47
48impl MergeBestTransactions {
49    /// Returns the next transaction from either pool with the higher priority.
50    fn next_best(&mut self) -> Option<BestTransactionWithPriority> {
51        if self.next_protocol_pool.is_none() {
52            self.next_protocol_pool = self.protocol_pool.next().map(|tx| {
53                let priority = TempoTipOrdering::default().priority(&tx.transaction, self.base_fee);
54                (tx, priority)
55            });
56        }
57        if self.next_aa_2d_pool.is_none() {
58            self.next_aa_2d_pool = self.aa_2d_pool.next_tx_and_priority();
59        }
60
61        match (&mut self.next_protocol_pool, &mut self.next_aa_2d_pool) {
62            (None, None) => {
63                // both iters are done
64                None
65            }
66            // Only the protocol pool has an item - take it
67            (Some(_), None) => {
68                let (item, priority) = self.next_protocol_pool.take()?;
69                Some((item, priority))
70            }
71            // Only the AA2D pool has an item - take it
72            (None, Some(_)) => {
73                let (item, priority) = self.next_aa_2d_pool.take()?;
74                Some((item, priority))
75            }
76            // Both pools have items - compare priorities and take the higher one
77            (Some((_, protocol_priority)), Some((_, aa_2d_priority))) => {
78                // Higher priority value is better
79                if protocol_priority >= aa_2d_priority {
80                    let (item, priority) = self.next_protocol_pool.take()?;
81                    Some((item, priority))
82                } else {
83                    let (item, priority) = self.next_aa_2d_pool.take()?;
84                    Some((item, priority))
85                }
86            }
87        }
88    }
89}
90
91impl Iterator for MergeBestTransactions {
92    type Item = BestTransaction;
93
94    fn next(&mut self) -> Option<Self::Item> {
95        self.next_best().map(|(tx, _)| tx)
96    }
97
98    fn size_hint(&self) -> (usize, Option<usize>) {
99        let buffered = usize::from(self.next_protocol_pool.is_some())
100            + usize::from(self.next_aa_2d_pool.is_some());
101        let (protocol_lower, protocol_upper) = self.protocol_pool.size_hint();
102        let (aa_2d_lower, aa_2d_upper) = self.aa_2d_pool.size_hint();
103
104        (
105            buffered
106                .saturating_add(protocol_lower)
107                .saturating_add(aa_2d_lower),
108            protocol_upper
109                .zip(aa_2d_upper)
110                .and_then(|(protocol_upper, aa_2d_upper)| protocol_upper.checked_add(aa_2d_upper))
111                .and_then(|upper| upper.checked_add(buffered)),
112        )
113    }
114}
115
116impl BestTransactions for MergeBestTransactions {
117    fn mark_invalid(&mut self, transaction: &Self::Item, kind: InvalidPoolTransactionError) {
118        if transaction.transaction.is_aa_2d() {
119            self.aa_2d_pool.mark_invalid(transaction, kind);
120        } else {
121            self.protocol_pool.mark_invalid(transaction, kind);
122        }
123    }
124
125    fn no_updates(&mut self) {
126        self.protocol_pool.no_updates();
127        self.aa_2d_pool.no_updates();
128    }
129
130    fn set_skip_blobs(&mut self, skip_blobs: bool) {
131        self.protocol_pool.set_skip_blobs(skip_blobs);
132        self.aa_2d_pool.set_skip_blobs(skip_blobs);
133    }
134}
135
136/// A [`BestTransactions`] wrapper that tracks execution state changes and skips
137/// transactions that would fail due to state mutations from previously
138/// included transactions.
139pub struct StateAwareBestTransactions<I> {
140    inner: I,
141    /// Tracks decreased TIP20 balance slots: `(token_address, slot) -> new_balance`.
142    /// Updated after each executed transaction. Used to check if a candidate
143    /// transaction's fee payer can still cover its fee cost.
144    decreased_balances: HashMap<(Address, U256), U256>,
145}
146
147impl<I> StateAwareBestTransactions<I>
148where
149    I: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
150{
151    /// Wraps an existing [`BestTransactions`] iterator.
152    pub fn new(inner: I) -> Self {
153        Self {
154            inner,
155            decreased_balances: HashMap::default(),
156        }
157    }
158
159    /// Processes a new transaction execution result and collects any relevant
160    /// state changes that might affect other transactions validity.
161    pub fn on_new_result(&mut self, result: &TempoTxResult) {
162        for (&address, account) in &result.result().state {
163            if !is_tip20_prefix(address) {
164                continue;
165            }
166
167            for (&slot, storage_slot) in &account.storage {
168                if storage_slot.present_value < storage_slot.original_value {
169                    self.decreased_balances
170                        .insert((address, slot), storage_slot.present_value);
171                } else if let Some(balance) = self.decreased_balances.get_mut(&(address, slot)) {
172                    *balance = storage_slot.present_value;
173                }
174            }
175        }
176    }
177}
178
179impl<I> Iterator for StateAwareBestTransactions<I>
180where
181    I: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
182{
183    type Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>;
184
185    fn next(&mut self) -> Option<Self::Item> {
186        loop {
187            let tx = self.inner.next()?;
188
189            let Some(key) = tx.transaction.fee_balance_slot() else {
190                debug_assert!(false, "pool transaction must have cached fee_balance_slot");
191                continue;
192            };
193
194            if let Some(&balance) = self.decreased_balances.get(&key)
195                && balance < tx.transaction.fee_token_cost()
196            {
197                self.inner.mark_invalid(
198                    &tx,
199                    InvalidPoolTransactionError::Consensus(
200                        InvalidTransactionError::InsufficientFunds(
201                            (balance, tx.transaction.fee_token_cost()).into(),
202                        ),
203                    ),
204                );
205                continue;
206            }
207
208            return Some(tx);
209        }
210    }
211}
212
213impl<I> BestTransactions for StateAwareBestTransactions<I>
214where
215    I: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>> + Send,
216{
217    fn mark_invalid(&mut self, transaction: &Self::Item, kind: InvalidPoolTransactionError) {
218        self.inner.mark_invalid(transaction, kind);
219    }
220
221    fn no_updates(&mut self) {
222        self.inner.no_updates();
223    }
224
225    fn set_skip_blobs(&mut self, skip_blobs: bool) {
226        self.inner.set_skip_blobs(skip_blobs);
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::{
234        ordering::TempoTipOrdering,
235        test_utils::{TxBuilder, wrap_valid_tx},
236        tt_2d_pool::AA2dPool,
237    };
238    use alloy_primitives::Address;
239    use futures::executor::block_on;
240    use reth_primitives_traits::transaction::error::InvalidTransactionError;
241    use reth_transaction_pool::{
242        Pool, PoolConfig, TransactionOrigin, TransactionPool, blobstore::InMemoryBlobStore,
243        test_utils::OkValidator,
244    };
245    use std::sync::Arc;
246    use tempo_chainspec::{hardfork::TempoHardfork, spec::TEMPO_T1_BASE_FEE};
247
248    type TestTx = Arc<ValidPoolTransaction<TempoPooledTransaction>>;
249
250    fn tx_with_nonce_key(nonce_key: U256, sender: Address, nonce: u64, priority: u128) -> TestTx {
251        Arc::new(wrap_valid_tx(
252            TxBuilder::aa(sender)
253                .nonce_key(nonce_key)
254                .nonce(nonce)
255                .max_priority_fee(priority)
256                .max_fee(u128::from(TEMPO_T1_BASE_FEE) + priority)
257                .build(),
258            TransactionOrigin::External,
259        ))
260    }
261
262    fn protocol_tx(nonce: u64, priority: u128) -> TestTx {
263        protocol_tx_for_sender(Address::random(), nonce, priority)
264    }
265
266    fn protocol_tx_for_sender(sender: Address, nonce: u64, priority: u128) -> TestTx {
267        tx_with_nonce_key(U256::ZERO, sender, nonce, priority)
268    }
269
270    fn aa_2d_tx(nonce: u64, priority: u128) -> TestTx {
271        aa_2d_tx_for_sequence(Address::random(), nonce, priority)
272    }
273
274    fn aa_2d_tx_for_sequence(sender: Address, nonce: u64, priority: u128) -> TestTx {
275        tx_with_nonce_key(U256::from(1), sender, nonce, priority)
276    }
277
278    fn protocol_best_transactions(
279        txs: Vec<TestTx>,
280    ) -> Box<dyn BestTransactions<Item = BestTransaction>> {
281        let pool = Pool::new(
282            OkValidator::<TempoPooledTransaction>::default(),
283            TempoTipOrdering::default(),
284            InMemoryBlobStore::default(),
285            PoolConfig::default(),
286        );
287
288        let results = block_on(pool.add_transactions(
289            TransactionOrigin::External,
290            txs.into_iter().map(|tx| tx.transaction.clone()).collect(),
291        ));
292        assert!(
293            results.iter().all(Result::is_ok),
294            "all protocol transactions must be added successfully: {results:?}"
295        );
296        Box::new(pool.inner().best_transactions())
297    }
298
299    fn aa_2d_best_transactions(txs: Vec<TestTx>) -> BestAA2dTransactions {
300        let mut pool = AA2dPool::default();
301        let mut on_chain_nonces: HashMap<crate::tt_2d_pool::AASequenceId, u64> = HashMap::default();
302        for tx in &txs {
303            let id = tx
304                .transaction
305                .aa_transaction_id()
306                .expect("AA2D transaction must have an AA transaction id");
307            on_chain_nonces
308                .entry(id.seq_id)
309                .and_modify(|nonce: &mut u64| *nonce = (*nonce).min(id.nonce))
310                .or_insert(id.nonce);
311        }
312
313        pool.set_base_fee(TEMPO_T1_BASE_FEE);
314        for tx in txs {
315            let id = tx
316                .transaction
317                .aa_transaction_id()
318                .expect("AA2D transaction must have an AA transaction id");
319            let on_chain_nonce = on_chain_nonces[&id.seq_id];
320            pool.add_transaction(tx, on_chain_nonce, TempoHardfork::T1)
321                .expect("AA2D transaction must be added successfully");
322        }
323        pool.best_transactions()
324    }
325
326    fn merged_best_transactions(
327        protocol_txs: Vec<TestTx>,
328        aa_2d_txs: Vec<TestTx>,
329    ) -> MergeBestTransactions {
330        MergeBestTransactions::new(
331            protocol_best_transactions(protocol_txs),
332            aa_2d_best_transactions(aa_2d_txs),
333            TEMPO_T1_BASE_FEE,
334        )
335    }
336
337    #[test]
338    fn test_merge_best_transactions_basic() {
339        // Create two mock iterators with different priorities
340        // Left: priorities [10, 5, 3]
341        // Right: priorities [8, 4, 1]
342        // Expected order: [10, 8, 5, 4, 3, 1]
343        let tx_a = protocol_tx(0, 10);
344        let tx_b = protocol_tx(1, 5);
345        let tx_c = protocol_tx(2, 3);
346        let tx_d = aa_2d_tx(3, 8);
347        let tx_e = aa_2d_tx(4, 4);
348        let tx_f = aa_2d_tx(5, 1);
349        let mut merged = merged_best_transactions(
350            vec![tx_a.clone(), tx_b.clone(), tx_c.clone()],
351            vec![tx_d.clone(), tx_e.clone(), tx_f.clone()],
352        );
353
354        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash())); // priority 10
355        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_d.hash())); // priority 8
356        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_b.hash())); // priority 5
357        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_e.hash())); // priority 4
358        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_c.hash())); // priority 3
359        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_f.hash())); // priority 1
360        assert!(merged.next().is_none());
361    }
362
363    #[test]
364    fn test_merge_best_transactions_size_hint() {
365        let protocol_sender = Address::random();
366        let protocol_tx_0 = protocol_tx_for_sender(protocol_sender, 0, 10);
367        let protocol_tx_1 = protocol_tx_for_sender(protocol_sender, 1, 9);
368        let aa_2d_tx = aa_2d_tx(0, 8);
369        let mut merged = merged_best_transactions(
370            vec![protocol_tx_0.clone(), protocol_tx_1.clone()],
371            vec![aa_2d_tx.clone()],
372        );
373        merged.no_updates();
374
375        assert_eq!(merged.size_hint(), (0, Some(3)));
376
377        assert_eq!(
378            merged.next().map(|tx| *tx.hash()),
379            Some(*protocol_tx_0.hash())
380        );
381        assert_eq!(merged.size_hint(), (1, Some(2)));
382
383        assert_eq!(
384            merged.next().map(|tx| *tx.hash()),
385            Some(*protocol_tx_1.hash())
386        );
387        assert_eq!(merged.size_hint(), (1, Some(1)));
388
389        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*aa_2d_tx.hash()));
390        assert_eq!(merged.size_hint(), (0, Some(0)));
391    }
392
393    #[test]
394    fn test_merge_best_transactions_empty_left() {
395        // Left iterator is empty
396        let tx_a = aa_2d_tx(0, 10);
397        let tx_b = aa_2d_tx(1, 5);
398        let mut merged = merged_best_transactions(vec![], vec![tx_a.clone(), tx_b.clone()]);
399
400        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash()));
401        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_b.hash()));
402        assert!(merged.next().is_none());
403    }
404
405    #[test]
406    fn test_merge_best_transactions_empty_right() {
407        // Right iterator is empty
408        let tx_a = protocol_tx(0, 10);
409        let tx_b = protocol_tx(1, 5);
410        let mut merged = merged_best_transactions(vec![tx_a.clone(), tx_b.clone()], vec![]);
411
412        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash()));
413        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_b.hash()));
414        assert!(merged.next().is_none());
415    }
416
417    #[test]
418    fn test_merge_best_transactions_both_empty() {
419        let mut merged = merged_best_transactions(vec![], vec![]);
420
421        assert!(merged.next().is_none());
422    }
423
424    #[test]
425    fn test_merge_best_transactions_equal_priorities() {
426        // When priorities are equal, left should be preferred (based on >= comparison)
427        let tx_a = protocol_tx(0, 10);
428        let tx_b = protocol_tx(1, 5);
429        let tx_c = aa_2d_tx(2, 10);
430        let tx_d = aa_2d_tx(3, 5);
431        let mut merged = merged_best_transactions(
432            vec![tx_a.clone(), tx_b.clone()],
433            vec![tx_c.clone(), tx_d.clone()],
434        );
435
436        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash())); // equal priority, left preferred
437        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_c.hash()));
438        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_b.hash())); // equal priority, left preferred
439        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_d.hash()));
440        assert!(merged.next().is_none());
441    }
442
443    // ============================================
444    // Single item tests
445    // ============================================
446
447    #[test]
448    fn test_merge_best_transactions_single_left() {
449        let tx_a = protocol_tx(0, 10);
450        let mut merged = merged_best_transactions(vec![tx_a.clone()], vec![]);
451
452        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash()));
453        assert!(merged.next().is_none());
454    }
455
456    #[test]
457    fn test_merge_best_transactions_single_right() {
458        let tx_a = aa_2d_tx(0, 10);
459        let mut merged = merged_best_transactions(vec![], vec![tx_a.clone()]);
460
461        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*tx_a.hash()));
462        assert!(merged.next().is_none());
463    }
464
465    // ============================================
466    // Interleaved priority tests
467    // ============================================
468
469    #[test]
470    fn test_merge_best_transactions_interleaved() {
471        // Left has higher odd positions, right has higher even positions
472        let l1 = protocol_tx(0, 9);
473        let l2 = protocol_tx(1, 7);
474        let l3 = protocol_tx(2, 5);
475        let r1 = aa_2d_tx(3, 10);
476        let r2 = aa_2d_tx(4, 6);
477        let r3 = aa_2d_tx(5, 4);
478        let mut merged = merged_best_transactions(
479            vec![l1.clone(), l2.clone(), l3.clone()],
480            vec![r1.clone(), r2.clone(), r3.clone()],
481        );
482
483        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*r1.hash())); // 10
484        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l1.hash())); // 9
485        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l2.hash())); // 7
486        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*r2.hash())); // 6
487        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l3.hash())); // 5
488        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*r3.hash())); // 4
489        assert!(merged.next().is_none());
490    }
491
492    #[test]
493    fn test_mark_invalid_routes_aa_2d_to_right_pool() {
494        // Invalidating an AA2D tx must NOT propagate to the
495        // left-side (protocol) pool.
496        let aa_2d_sender = Address::random();
497        let l1 = protocol_tx(0, 9);
498        let l2 = protocol_tx(1, 7);
499        let r1 = aa_2d_tx_for_sequence(aa_2d_sender, 0, 10);
500        let r2 = aa_2d_tx_for_sequence(aa_2d_sender, 1, 8);
501        let mut merged =
502            merged_best_transactions(vec![l1.clone(), l2.clone()], vec![r1.clone(), r2]);
503
504        // Right has highest priority, so R1 is yielded first
505        let first = merged.next().unwrap();
506        assert_eq!(*first.hash(), *r1.hash());
507
508        // Simulate payload builder marking R1 as invalid
509        let kind =
510            InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported);
511        merged.mark_invalid(&first, kind);
512
513        // The AA2D descendant must be skipped, while protocol txs still yield.
514        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l1.hash()));
515        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l2.hash()));
516        assert!(merged.next().is_none());
517    }
518
519    #[test]
520    fn test_mark_invalid_routes_aa_2d_after_later_protocol_next() {
521        let aa_2d_sender = Address::random();
522        let protocol_sender = Address::random();
523        let l1 = protocol_tx_for_sender(protocol_sender, 0, 9);
524        let l2 = protocol_tx_for_sender(protocol_sender, 1, 7);
525        let r1 = aa_2d_tx_for_sequence(aa_2d_sender, 0, 10);
526        let mut merged = merged_best_transactions(vec![l1.clone(), l2.clone()], vec![r1.clone()]);
527        let first = merged.next().unwrap();
528        let second = merged.next().unwrap();
529
530        assert_eq!(*first.hash(), *r1.hash());
531        assert_eq!(*second.hash(), *l1.hash());
532
533        let kind =
534            InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported);
535        merged.mark_invalid(&first, kind);
536
537        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*l2.hash()));
538        assert!(merged.next().is_none());
539    }
540
541    #[test]
542    fn test_mark_invalid_routes_protocol_aa_to_left_pool() {
543        let protocol_sender = Address::random();
544        let left_tx = protocol_tx_for_sender(protocol_sender, 0, 10);
545        let left_descendant = protocol_tx_for_sender(protocol_sender, 1, 9);
546        let right_tx = aa_2d_tx(0, 8);
547        assert!(left_tx.transaction.is_aa());
548        assert!(!left_tx.transaction.is_aa_2d());
549        assert!(right_tx.transaction.is_aa_2d());
550
551        let mut merged = merged_best_transactions(
552            vec![left_tx.clone(), left_descendant],
553            vec![right_tx.clone()],
554        );
555        let first = merged.next().unwrap();
556        assert_eq!(*first.hash(), *left_tx.hash());
557
558        let kind =
559            InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported);
560        merged.mark_invalid(&first, kind);
561
562        assert_eq!(merged.next().map(|tx| *tx.hash()), Some(*right_tx.hash()));
563        assert!(merged.next().is_none());
564    }
565}