Skip to main content

reth_transaction_pool/pool/
blob.rs

1use super::txpool::PendingFees;
2use crate::{
3    identifier::{SenderId, TransactionId},
4    pool::size::SizeTracker,
5    traits::BestTransactionsAttributes,
6    PoolTransaction, SubPoolLimit, ValidPoolTransaction,
7};
8use std::{
9    cmp::Ordering,
10    collections::{BTreeMap, BTreeSet},
11    ops::Bound::Unbounded,
12    sync::Arc,
13};
14
15/// A set of validated blob transactions in the pool that are __not pending__.
16///
17/// The purpose of this pool is to keep track of blob transactions that are queued and to evict the
18/// worst blob transactions once the sub-pool is full.
19///
20/// This expects that certain constraints are met:
21///   - blob transactions are always gapless
22#[derive(Debug, Clone)]
23pub struct BlobTransactions<T: PoolTransaction> {
24    /// Keeps track of transactions inserted in the pool.
25    ///
26    /// This way we can determine when transactions were submitted to the pool.
27    submission_id: u64,
28    /// _All_ Transactions that are currently inside the pool grouped by their identifier.
29    by_id: BTreeMap<TransactionId, BlobTransaction<T>>,
30    /// _All_ transactions sorted by blob priority.
31    all: BTreeSet<BlobTransaction<T>>,
32    /// Keeps track of the current fees, so transaction priority can be calculated on insertion.
33    pending_fees: PendingFees,
34    /// Keeps track of the size of this pool.
35    ///
36    /// See also [`reth_primitives_traits::InMemorySize::size`].
37    size_of: SizeTracker,
38}
39
40// === impl BlobTransactions ===
41
42impl<T: PoolTransaction> BlobTransactions<T> {
43    /// Adds a new transactions to the pending queue.
44    ///
45    /// # Panics
46    ///
47    ///   - If the transaction is not a blob tx.
48    ///   - If the transaction is already included.
49    pub fn add_transaction(&mut self, tx: Arc<ValidPoolTransaction<T>>) {
50        assert!(tx.is_eip4844(), "transaction is not a blob tx");
51        let id = *tx.id();
52        assert!(!self.contains(&id), "transaction already included {:?}", self.get(&id).unwrap());
53        let submission_id = self.next_id();
54
55        // keep track of size
56        self.size_of += tx.size();
57
58        // set transaction, which will also calculate priority based on current pending fees
59        let transaction = BlobTransaction::new(tx, submission_id, &self.pending_fees);
60
61        self.by_id.insert(id, transaction.clone());
62        self.all.insert(transaction);
63    }
64
65    const fn next_id(&mut self) -> u64 {
66        let id = self.submission_id;
67        self.submission_id = self.submission_id.wrapping_add(1);
68        id
69    }
70
71    /// Returns an iterator over all transactions in the pool
72    pub(crate) fn all(&self) -> impl ExactSizeIterator<Item = Arc<ValidPoolTransaction<T>>> + '_ {
73        self.by_id.values().map(|tx| tx.transaction.clone())
74    }
75
76    /// Returns an iterator over all transactions for the given sender, using a `BTree` range
77    /// query.
78    pub(crate) fn txs_by_sender(
79        &self,
80        sender: SenderId,
81    ) -> impl Iterator<Item = Arc<ValidPoolTransaction<T>>> + '_ {
82        self.by_id
83            .range((sender.start_bound(), Unbounded))
84            .take_while(move |(other, _)| sender == other.sender)
85            .map(|(_, tx)| Arc::clone(&tx.transaction))
86    }
87
88    /// Removes the transaction from the pool
89    pub(crate) fn remove_transaction(
90        &mut self,
91        id: &TransactionId,
92    ) -> Option<Arc<ValidPoolTransaction<T>>> {
93        // remove from queues
94        let tx = self.by_id.remove(id)?;
95
96        self.all.remove(&tx);
97
98        // keep track of size
99        self.size_of -= tx.transaction.size();
100
101        Some(tx.transaction)
102    }
103
104    /// Returns all transactions that satisfy the given basefee and blobfee.
105    ///
106    /// Note: This does not remove any of the transactions from the pool.
107    pub(crate) fn satisfy_attributes(
108        &self,
109        best_transactions_attributes: BestTransactionsAttributes,
110    ) -> Vec<Arc<ValidPoolTransaction<T>>> {
111        let mut transactions = Vec::new();
112        {
113            // short path if blob_fee is None in provided best transactions attributes
114            if let Some(blob_fee_to_satisfy) =
115                best_transactions_attributes.blob_fee.map(|fee| fee as u128)
116            {
117                let mut iter = self.by_id.iter().peekable();
118
119                while let Some((id, tx)) = iter.next() {
120                    if tx.transaction.max_fee_per_blob_gas().unwrap_or_default() <
121                        blob_fee_to_satisfy ||
122                        tx.transaction.max_fee_per_gas() <
123                            best_transactions_attributes.basefee as u128
124                    {
125                        // does not satisfy the blob fee or base fee
126                        // still parked in blob pool -> skip descendant transactions
127                        'this: while let Some((peek, _)) = iter.peek() {
128                            if peek.sender != id.sender {
129                                break 'this
130                            }
131                            iter.next();
132                        }
133                    } else {
134                        transactions.push(tx.transaction.clone());
135                    }
136                }
137            }
138        }
139        transactions
140    }
141
142    /// Returns true if the pool exceeds the given limit
143    #[inline]
144    pub(crate) fn exceeds(&self, limit: &SubPoolLimit) -> bool {
145        limit.is_exceeded(self.len(), self.size())
146    }
147
148    /// The reported size of all transactions in this pool.
149    pub(crate) fn size(&self) -> usize {
150        self.size_of.into()
151    }
152
153    /// Number of transactions in the entire pool
154    pub(crate) fn len(&self) -> usize {
155        self.by_id.len()
156    }
157
158    /// Returns whether the pool is empty
159    #[cfg(test)]
160    pub(crate) fn is_empty(&self) -> bool {
161        self.by_id.is_empty()
162    }
163
164    /// Returns all transactions which:
165    ///  * have a `max_fee_per_blob_gas` greater than or equal to the given `blob_fee`, _and_
166    ///  * have a `max_fee_per_gas` greater than or equal to the given `base_fee`
167    fn satisfy_pending_fee_ids(&self, pending_fees: &PendingFees) -> Vec<TransactionId> {
168        let mut transactions = Vec::new();
169        {
170            let mut iter = self.by_id.iter().peekable();
171
172            while let Some((id, tx)) = iter.next() {
173                if tx.transaction.max_fee_per_blob_gas() < Some(pending_fees.blob_fee) ||
174                    tx.transaction.max_fee_per_gas() < pending_fees.base_fee as u128
175                {
176                    // still parked in blob pool -> skip descendant transactions
177                    'this: while let Some((peek, _)) = iter.peek() {
178                        if peek.sender != id.sender {
179                            break 'this
180                        }
181                        iter.next();
182                    }
183                } else {
184                    transactions.push(*id);
185                }
186            }
187        }
188        transactions
189    }
190
191    /// Resorts the transactions in the pool based on the pool's current [`PendingFees`].
192    pub(crate) fn reprioritize(&mut self) {
193        // mem::take to modify without allocating, then collect to rebuild the BTreeSet
194        self.all = std::mem::take(&mut self.all)
195            .into_iter()
196            .map(|mut tx| {
197                tx.update_priority(&self.pending_fees);
198                tx
199            })
200            .collect();
201
202        // we need to update `by_id` as well because removal from `all` can only happen if the
203        // `BlobTransaction`s in each struct are consistent
204        for tx in self.by_id.values_mut() {
205            tx.update_priority(&self.pending_fees);
206        }
207    }
208
209    /// Removes all transactions (and their descendants) which:
210    ///  * have a `max_fee_per_blob_gas` greater than or equal to the given `blob_fee`, _and_
211    ///  * have a `max_fee_per_gas` greater than or equal to the given `base_fee`
212    ///
213    /// This also sets the [`PendingFees`] for the pool, resorting transactions based on their
214    /// updated priority.
215    ///
216    /// Note: the transactions are not returned in a particular order.
217    pub(crate) fn enforce_pending_fees(
218        &mut self,
219        pending_fees: &PendingFees,
220    ) -> Vec<Arc<ValidPoolTransaction<T>>> {
221        let removed = self
222            .satisfy_pending_fee_ids(pending_fees)
223            .into_iter()
224            .map(|id| self.remove_transaction(&id).expect("transaction exists"))
225            .collect();
226
227        // Update pending fees and reprioritize
228        self.pending_fees = pending_fees.clone();
229        self.reprioritize();
230
231        removed
232    }
233
234    /// Removes transactions until the pool satisfies its [`SubPoolLimit`].
235    ///
236    /// This is done by removing transactions according to their ordering in the pool, defined by
237    /// the [`BlobOrd`] struct.
238    ///
239    /// Removed transactions are returned in the order they were removed.
240    pub fn truncate_pool(&mut self, limit: SubPoolLimit) -> Vec<Arc<ValidPoolTransaction<T>>> {
241        let mut removed = Vec::new();
242
243        while self.exceeds(&limit) {
244            let tx = self.all.last().expect("pool is not empty");
245            let id = *tx.transaction.id();
246            removed.push(self.remove_transaction(&id).expect("transaction exists"));
247        }
248
249        removed
250    }
251
252    /// Returns `true` if the transaction with the given id is already included in this pool.
253    pub(crate) fn contains(&self, id: &TransactionId) -> bool {
254        self.by_id.contains_key(id)
255    }
256
257    /// Retrieves a transaction with the given ID from the pool, if it exists.
258    fn get(&self, id: &TransactionId) -> Option<&BlobTransaction<T>> {
259        self.by_id.get(id)
260    }
261
262    /// Asserts that the bijection between `by_id` and `all` is valid.
263    #[cfg(any(test, feature = "test-utils"))]
264    pub(crate) fn assert_invariants(&self) {
265        assert_eq!(self.by_id.len(), self.all.len(), "by_id.len() != all.len()");
266    }
267}
268
269impl<T: PoolTransaction> Default for BlobTransactions<T> {
270    fn default() -> Self {
271        Self {
272            submission_id: 0,
273            by_id: Default::default(),
274            all: Default::default(),
275            size_of: Default::default(),
276            pending_fees: Default::default(),
277        }
278    }
279}
280
281/// A transaction that is ready to be included in a block.
282#[derive(Debug)]
283struct BlobTransaction<T: PoolTransaction> {
284    /// Actual blob transaction.
285    transaction: Arc<ValidPoolTransaction<T>>,
286    /// The value that determines the order of this transaction.
287    ord: BlobOrd,
288}
289
290impl<T: PoolTransaction> BlobTransaction<T> {
291    /// Creates a new blob transaction, based on the pool transaction, submission id, and current
292    /// pending fees.
293    pub(crate) fn new(
294        transaction: Arc<ValidPoolTransaction<T>>,
295        submission_id: u64,
296        pending_fees: &PendingFees,
297    ) -> Self {
298        let priority = blob_tx_priority(
299            transaction.max_fee_per_blob_gas().unwrap_or_default(),
300            pending_fees.blob_fee,
301            transaction.max_fee_per_gas(),
302            pending_fees.base_fee as u128,
303        );
304        let ord = BlobOrd { priority, submission_id };
305        Self { transaction, ord }
306    }
307
308    /// Updates the priority for the transaction based on the current pending fees.
309    pub(crate) fn update_priority(&mut self, pending_fees: &PendingFees) {
310        self.ord.priority = blob_tx_priority(
311            self.transaction.max_fee_per_blob_gas().unwrap_or_default(),
312            pending_fees.blob_fee,
313            self.transaction.max_fee_per_gas(),
314            pending_fees.base_fee as u128,
315        );
316    }
317}
318
319impl<T: PoolTransaction> Clone for BlobTransaction<T> {
320    fn clone(&self) -> Self {
321        Self { transaction: self.transaction.clone(), ord: self.ord.clone() }
322    }
323}
324
325impl<T: PoolTransaction> Eq for BlobTransaction<T> {}
326
327impl<T: PoolTransaction> PartialEq<Self> for BlobTransaction<T> {
328    fn eq(&self, other: &Self) -> bool {
329        self.cmp(other) == Ordering::Equal
330    }
331}
332
333impl<T: PoolTransaction> PartialOrd<Self> for BlobTransaction<T> {
334    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
335        Some(self.cmp(other))
336    }
337}
338
339impl<T: PoolTransaction> Ord for BlobTransaction<T> {
340    fn cmp(&self, other: &Self) -> Ordering {
341        self.ord.cmp(&other.ord)
342    }
343}
344
345/// This is the log base 2 of 1.125, which we'll use to calculate the priority
346const LOG_2_1_125: f64 = 0.16992500144231237;
347
348/// The blob step function, attempting to compute the delta given the `max_tx_fee`, and
349/// `current_fee`.
350///
351/// The `max_tx_fee` is the maximum fee that the transaction is willing to pay, this
352/// would be the priority fee for the EIP1559 component of transaction fees, and the blob fee cap
353/// for the blob component of transaction fees.
354///
355/// The `current_fee` is the current value of the fee, this would be the base fee for the EIP1559
356/// component, and the blob fee (computed from the current head) for the blob component.
357///
358/// This is supposed to get the number of fee jumps required to get from the current fee to the fee
359/// cap, or where the transaction would not be executable any more.
360///
361/// A positive value means that the transaction will remain executable unless the current fee
362/// increases.
363///
364/// A negative value means that the transaction is currently not executable, and requires the
365/// current fee to decrease by some number of jumps before the max fee is greater than the current
366/// fee.
367pub fn fee_delta(max_tx_fee: u128, current_fee: u128) -> i64 {
368    if max_tx_fee == current_fee {
369        // if these are equal, then there's no fee jump
370        return 0
371    }
372
373    let max_tx_fee_jumps = if max_tx_fee == 0 {
374        // we can't take log2 of 0, so we set this to zero here
375        0f64
376    } else {
377        (max_tx_fee.ilog2() as f64) / LOG_2_1_125
378    };
379
380    let current_fee_jumps = if current_fee == 0 {
381        // we can't take log2 of 0, so we set this to zero here
382        0f64
383    } else {
384        (current_fee.ilog2() as f64) / LOG_2_1_125
385    };
386
387    // jumps = log1.125(txfee) - log1.125(basefee)
388    let jumps = max_tx_fee_jumps - current_fee_jumps;
389
390    // delta = sign(jumps) * log(abs(jumps))
391    match (jumps as i64).cmp(&0) {
392        Ordering::Equal => {
393            // can't take ilog2 of 0
394            0
395        }
396        Ordering::Greater => (jumps.ceil() as i64).ilog2() as i64,
397        Ordering::Less => -((-jumps.floor() as i64).ilog2() as i64),
398    }
399}
400
401/// Returns the priority for the transaction, based on the "delta" blob fee and priority fee.
402pub fn blob_tx_priority(
403    blob_fee_cap: u128,
404    blob_fee: u128,
405    max_priority_fee: u128,
406    base_fee: u128,
407) -> i64 {
408    let delta_blob_fee = fee_delta(blob_fee_cap, blob_fee);
409    let delta_priority_fee = fee_delta(max_priority_fee, base_fee);
410
411    // TODO: this could be u64:
412    // * if all are positive, zero is returned
413    // * if all are negative, the min negative value is returned
414    // * if some are positive and some are negative, the min negative value is returned
415    //
416    // the BlobOrd could then just be a u64, and higher values represent worse transactions (more
417    // jumps for one of the fees until the cap satisfies)
418    //
419    // priority = min(delta-basefee, delta-blobfee, 0)
420    delta_blob_fee.min(delta_priority_fee).min(0)
421}
422
423/// A struct used to determine the ordering for a specific blob transaction in the pool. This uses
424/// a `priority` value to determine the ordering, and uses the `submission_id` to break ties.
425///
426/// The `priority` value is calculated using the [`blob_tx_priority`] function, and should be
427/// re-calculated on each block.
428#[derive(Debug, Clone)]
429pub struct BlobOrd {
430    /// Identifier that tags when transaction was submitted in the pool.
431    pub(crate) submission_id: u64,
432    /// The priority for this transaction, calculated using the [`blob_tx_priority`] function,
433    /// taking into account both the blob and priority fee.
434    pub(crate) priority: i64,
435}
436
437impl Eq for BlobOrd {}
438
439impl PartialEq<Self> for BlobOrd {
440    fn eq(&self, other: &Self) -> bool {
441        self.cmp(other) == Ordering::Equal
442    }
443}
444
445impl PartialOrd<Self> for BlobOrd {
446    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
447        Some(self.cmp(other))
448    }
449}
450
451impl Ord for BlobOrd {
452    /// Compares two `BlobOrd` instances.
453    ///
454    /// The comparison is performed in reverse order based on the priority field. This is
455    /// because transactions with larger negative values in the priority field will take more fee
456    /// jumps, making them take longer to become executable. Therefore, transactions with lower
457    /// ordering should return `Greater`, ensuring they are evicted first.
458    ///
459    /// If the priority values are equal, the submission ID is used to break ties.
460    fn cmp(&self, other: &Self) -> Ordering {
461        other
462            .priority
463            .cmp(&self.priority)
464            .then_with(|| self.submission_id.cmp(&other.submission_id))
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use crate::test_utils::{MockTransaction, MockTransactionFactory};
472
473    /// Represents the fees for a single transaction, which will be built inside of a test.
474    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
475    struct TransactionFees {
476        /// The blob fee cap for the transaction.
477        max_blob_fee: u128,
478        /// The max priority fee for the transaction.
479        max_priority_fee_per_gas: u128,
480        /// The base fee for the transaction.
481        max_fee_per_gas: u128,
482    }
483
484    /// Represents an ordering of transactions based on their fees and the current network fees.
485    #[derive(Debug, Clone)]
486    struct TransactionOrdering {
487        /// The transaction fees, in the order that they're expected to be returned
488        fees: Vec<TransactionFees>,
489        /// The network fees
490        network_fees: PendingFees,
491    }
492
493    #[test]
494    fn test_blob_ordering() {
495        // Tests are from:
496        // <https://github.com/ethereum/go-ethereum/blob/e91cdb49beb4b2a3872b5f2548bf2d6559e4f561/core/txpool/blobpool/evictheap_test.go>
497        let mut factory = MockTransactionFactory::default();
498
499        let vectors = vec![
500            // If everything is above basefee and blobfee, order by miner tip
501            TransactionOrdering {
502                fees: vec![
503                    TransactionFees {
504                        max_blob_fee: 2,
505                        max_priority_fee_per_gas: 0,
506                        max_fee_per_gas: 2,
507                    },
508                    TransactionFees {
509                        max_blob_fee: 3,
510                        max_priority_fee_per_gas: 1,
511                        max_fee_per_gas: 1,
512                    },
513                    TransactionFees {
514                        max_blob_fee: 1,
515                        max_priority_fee_per_gas: 2,
516                        max_fee_per_gas: 3,
517                    },
518                ],
519                network_fees: PendingFees { base_fee: 0, blob_fee: 0 },
520            },
521            // If only basefees are used (blob fee matches with network), return the ones
522            // above the basefee first (best priority = 0), then the ones furthest below
523            // the basefee last (worst priority). Ties broken by submission_id.
524            TransactionOrdering {
525                fees: vec![
526                    TransactionFees {
527                        max_blob_fee: 0,
528                        max_priority_fee_per_gas: 1,
529                        max_fee_per_gas: 2000,
530                    },
531                    TransactionFees {
532                        max_blob_fee: 0,
533                        max_priority_fee_per_gas: 2,
534                        max_fee_per_gas: 2000,
535                    },
536                    TransactionFees {
537                        max_blob_fee: 0,
538                        max_priority_fee_per_gas: 3,
539                        max_fee_per_gas: 2000,
540                    },
541                    TransactionFees {
542                        max_blob_fee: 0,
543                        max_priority_fee_per_gas: 50,
544                        max_fee_per_gas: 1000,
545                    },
546                    TransactionFees {
547                        max_blob_fee: 0,
548                        max_priority_fee_per_gas: 100,
549                        max_fee_per_gas: 1000,
550                    },
551                    TransactionFees {
552                        max_blob_fee: 0,
553                        max_priority_fee_per_gas: 50,
554                        max_fee_per_gas: 500,
555                    },
556                    TransactionFees {
557                        max_blob_fee: 0,
558                        max_priority_fee_per_gas: 100,
559                        max_fee_per_gas: 500,
560                    },
561                ],
562                network_fees: PendingFees { base_fee: 1999, blob_fee: 0 },
563            },
564            // If only blobfees are used (base fee matches with network), return the ones
565            // above the blobfee first (best priority = 0), then the ones furthest below
566            // the blobfee last (worst priority). Ties broken by submission_id.
567            TransactionOrdering {
568                fees: vec![
569                    TransactionFees {
570                        max_blob_fee: 2000,
571                        max_priority_fee_per_gas: 1,
572                        max_fee_per_gas: 0,
573                    },
574                    TransactionFees {
575                        max_blob_fee: 2000,
576                        max_priority_fee_per_gas: 2,
577                        max_fee_per_gas: 0,
578                    },
579                    TransactionFees {
580                        max_blob_fee: 2000,
581                        max_priority_fee_per_gas: 3,
582                        max_fee_per_gas: 0,
583                    },
584                    TransactionFees {
585                        max_blob_fee: 1000,
586                        max_priority_fee_per_gas: 50,
587                        max_fee_per_gas: 0,
588                    },
589                    TransactionFees {
590                        max_blob_fee: 1000,
591                        max_priority_fee_per_gas: 100,
592                        max_fee_per_gas: 0,
593                    },
594                    TransactionFees {
595                        max_blob_fee: 500,
596                        max_priority_fee_per_gas: 50,
597                        max_fee_per_gas: 0,
598                    },
599                    TransactionFees {
600                        max_blob_fee: 500,
601                        max_priority_fee_per_gas: 100,
602                        max_fee_per_gas: 0,
603                    },
604                ],
605                network_fees: PendingFees { base_fee: 0, blob_fee: 1999 },
606            },
607            // If both basefee and blobfee are specified, sort by the larger distance
608            // of the two from the current network conditions.
609            //
610            // Basefee: 1000, Blobfee: 100
611            //
612            // Txs with blob_fee=80: fee_delta(80, 100) = 0 (ilog2 granularity) => priority 0
613            // Txs with blob_fee=63: fee_delta(63, 100) = -2 => priority -2
614            //
615            // Priority 0 txs come first (best), then priority -2 (worst).
616            // Within same priority, ties broken by submission_id.
617            TransactionOrdering {
618                fees: vec![
619                    TransactionFees {
620                        max_blob_fee: 80,
621                        max_priority_fee_per_gas: 4,
622                        max_fee_per_gas: 630,
623                    },
624                    TransactionFees {
625                        max_blob_fee: 80,
626                        max_priority_fee_per_gas: 1,
627                        max_fee_per_gas: 800,
628                    },
629                    TransactionFees {
630                        max_blob_fee: 63,
631                        max_priority_fee_per_gas: 3,
632                        max_fee_per_gas: 800,
633                    },
634                    TransactionFees {
635                        max_blob_fee: 63,
636                        max_priority_fee_per_gas: 2,
637                        max_fee_per_gas: 630,
638                    },
639                ],
640                network_fees: PendingFees { base_fee: 1000, blob_fee: 100 },
641            },
642        ];
643
644        for ordering in vectors {
645            // create a new pool each time
646            let mut pool = BlobTransactions::default();
647
648            // create tx from fees
649            let txs = ordering
650                .fees
651                .iter()
652                .map(|fees| {
653                    MockTransaction::eip4844()
654                        .with_blob_fee(fees.max_blob_fee)
655                        .with_priority_fee(fees.max_priority_fee_per_gas)
656                        .with_max_fee(fees.max_fee_per_gas)
657                })
658                .collect::<Vec<_>>();
659
660            for tx in &txs {
661                pool.add_transaction(factory.validated_arc(tx.clone()));
662            }
663
664            // update fees and resort the pool
665            pool.pending_fees = ordering.network_fees.clone();
666            pool.reprioritize();
667
668            // now iterate through the pool and make sure they're in the same order as the original
669            // fees - map to TransactionFees so it's easier to compare the ordering without having
670            // to see irrelevant fields
671            let actual_txs = pool
672                .all
673                .iter()
674                .map(|tx| TransactionFees {
675                    max_blob_fee: tx.transaction.max_fee_per_blob_gas().unwrap_or_default(),
676                    max_priority_fee_per_gas: tx.transaction.priority_fee_or_price(),
677                    max_fee_per_gas: tx.transaction.max_fee_per_gas(),
678                })
679                .collect::<Vec<_>>();
680            assert_eq!(
681                ordering.fees, actual_txs,
682                "ordering mismatch, expected: {:#?}, actual: {:#?}",
683                ordering.fees, actual_txs
684            );
685        }
686    }
687
688    #[test]
689    fn priority_tests() {
690        // Test vectors from:
691        // <https://github.com/ethereum/go-ethereum/blob/e91cdb49beb4b2a3872b5f2548bf2d6559e4f561/core/txpool/blobpool/priority_test.go#L27-L49>
692        let vectors = vec![
693            (7u128, 10u128, 2i64),
694            (17_200_000_000, 17_200_000_000, 0),
695            (9_853_941_692, 11_085_092_510, 0),
696            (11_544_106_391, 10_356_781_100, 0),
697            (17_200_000_000, 7, -7),
698            (7, 17_200_000_000, 7),
699        ];
700
701        for (base_fee, tx_fee, expected) in vectors {
702            let actual = fee_delta(tx_fee, base_fee);
703            assert_eq!(
704                actual, expected,
705                "fee_delta({tx_fee}, {base_fee}) = {actual}, expected: {expected}"
706            );
707        }
708    }
709
710    #[test]
711    fn test_empty_pool_operations() {
712        let mut pool: BlobTransactions<MockTransaction> = BlobTransactions::default();
713
714        // Ensure pool is empty
715        assert!(pool.is_empty());
716        assert_eq!(pool.len(), 0);
717        assert_eq!(pool.size(), 0);
718
719        // Attempt to remove a non-existent transaction
720        let non_existent_id = TransactionId::new(0.into(), 0);
721        assert!(pool.remove_transaction(&non_existent_id).is_none());
722
723        // Check contains method on empty pool
724        assert!(!pool.contains(&non_existent_id));
725    }
726
727    #[test]
728    fn test_transaction_removal() {
729        let mut factory = MockTransactionFactory::default();
730        let mut pool = BlobTransactions::default();
731
732        // Add a transaction
733        let tx = factory.validated_arc(MockTransaction::eip4844());
734        let tx_id = *tx.id();
735        pool.add_transaction(tx);
736
737        // Remove the transaction
738        let removed = pool.remove_transaction(&tx_id);
739        assert!(removed.is_some());
740        assert_eq!(*removed.unwrap().id(), tx_id);
741        assert!(pool.is_empty());
742    }
743
744    #[test]
745    fn test_satisfy_attributes_empty_pool() {
746        let pool: BlobTransactions<MockTransaction> = BlobTransactions::default();
747        let attributes = BestTransactionsAttributes { blob_fee: Some(100), basefee: 100 };
748        // Satisfy attributes on an empty pool should return an empty vector
749        let satisfied = pool.satisfy_attributes(attributes);
750        assert!(satisfied.is_empty());
751    }
752
753    #[test]
754    #[should_panic(expected = "transaction is not a blob tx")]
755    fn test_add_non_blob_transaction() {
756        // Ensure that adding a non-blob transaction causes a panic
757        let mut factory = MockTransactionFactory::default();
758        let mut pool = BlobTransactions::default();
759        let tx = factory.validated_arc(MockTransaction::eip1559()); // Not a blob transaction
760        pool.add_transaction(tx);
761    }
762
763    #[test]
764    #[should_panic(expected = "transaction already included")]
765    fn test_add_duplicate_blob_transaction() {
766        // Ensure that adding a duplicate blob transaction causes a panic
767        let mut factory = MockTransactionFactory::default();
768        let mut pool = BlobTransactions::default();
769        let tx = factory.validated_arc(MockTransaction::eip4844());
770        pool.add_transaction(tx.clone()); // First addition
771        pool.add_transaction(tx); // Attempt to add the same transaction again
772    }
773
774    #[test]
775    fn test_remove_transactions_until_limit() {
776        // Test truncating the pool until it satisfies the given size limit
777        let mut factory = MockTransactionFactory::default();
778        let mut pool = BlobTransactions::default();
779        let tx1 = factory.validated_arc(MockTransaction::eip4844().with_size(100));
780        let tx2 = factory.validated_arc(MockTransaction::eip4844().with_size(200));
781        let tx3 = factory.validated_arc(MockTransaction::eip4844().with_size(300));
782
783        // Add transactions to the pool
784        pool.add_transaction(tx1);
785        pool.add_transaction(tx2);
786        pool.add_transaction(tx3);
787
788        // Set a size limit that requires truncation
789        let limit = SubPoolLimit { max_txs: 2, max_size: 300 };
790        let removed = pool.truncate_pool(limit);
791
792        // Check that only one transaction was removed to satisfy the limit
793        assert_eq!(removed.len(), 1);
794        assert_eq!(pool.len(), 2);
795        assert!(pool.size() <= limit.max_size);
796    }
797
798    #[test]
799    fn test_empty_pool_invariants() {
800        // Ensure that the invariants hold for an empty pool
801        let pool: BlobTransactions<MockTransaction> = BlobTransactions::default();
802        pool.assert_invariants();
803        assert!(pool.is_empty());
804        assert_eq!(pool.size(), 0);
805        assert_eq!(pool.len(), 0);
806    }
807}