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