reth_transaction_pool/pool/
blob.rs

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