reth_transaction_pool/pool/
state.rs

1use crate::pool::QueuedReason;
2
3bitflags::bitflags! {
4    /// Marker to represents the current state of a transaction in the pool and from which the corresponding sub-pool is derived, depending on what bits are set.
5    ///
6    /// This mirrors [erigon's ephemeral state field](https://github.com/ledgerwatch/erigon/wiki/Transaction-Pool-Design#ordering-function).
7    ///
8    /// The [SubPool] the transaction belongs to is derived from its state and determined by the following sequential checks:
9    ///
10    /// - If it satisfies the [TxState::PENDING_POOL_BITS] it belongs in the pending sub-pool: [SubPool::Pending].
11    /// - If it is an EIP-4844 blob transaction it belongs in the blob sub-pool: [SubPool::Blob].
12    /// - If it satisfies the [TxState::BASE_FEE_POOL_BITS] it belongs in the base fee sub-pool: [SubPool::BaseFee].
13    ///
14    /// Otherwise, it belongs in the queued sub-pool: [SubPool::Queued].
15    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
16    pub(crate) struct TxState: u8 {
17        /// Set to `1` if all ancestor transactions are pending.
18        const NO_PARKED_ANCESTORS = 0b10000000;
19        /// Set to `1` if the transaction is either the next transaction of the sender (on chain nonce == tx.nonce) or all prior transactions are also present in the pool.
20        const NO_NONCE_GAPS = 0b01000000;
21        /// Bit derived from the sender's balance.
22        ///
23        /// Set to `1` if the sender's balance can cover the maximum cost for this transaction (`feeCap * gasLimit + value`).
24        /// This includes cumulative costs of prior transactions, which ensures that the sender has enough funds for all max cost of prior transactions.
25        const ENOUGH_BALANCE = 0b00100000;
26        /// Bit set to true if the transaction has a lower gas limit than the block's gas limit.
27        const NOT_TOO_MUCH_GAS = 0b00010000;
28        /// Covers the Dynamic fee requirement.
29        ///
30        /// Set to 1 if `maxFeePerGas` of the transaction meets the requirement of the pending block.
31        const ENOUGH_FEE_CAP_BLOCK = 0b00001000;
32        /// Covers the dynamic blob fee requirement, only relevant for EIP-4844 blob transactions.
33        ///
34        /// Set to 1 if `maxBlobFeePerGas` of the transaction meets the requirement of the pending block.
35        const ENOUGH_BLOB_FEE_CAP_BLOCK = 0b00000100;
36        /// Marks whether the transaction is a blob transaction.
37        ///
38        /// We track this as part of the state for simplicity, since blob transactions are handled differently and are mutually exclusive with normal transactions.
39        const BLOB_TRANSACTION = 0b00000010;
40
41        const PENDING_POOL_BITS = Self::NO_PARKED_ANCESTORS.bits() | Self::NO_NONCE_GAPS.bits() | Self::ENOUGH_BALANCE.bits() | Self::NOT_TOO_MUCH_GAS.bits() |  Self::ENOUGH_FEE_CAP_BLOCK.bits() | Self::ENOUGH_BLOB_FEE_CAP_BLOCK.bits();
42
43        const BASE_FEE_POOL_BITS = Self::NO_PARKED_ANCESTORS.bits() | Self::NO_NONCE_GAPS.bits() | Self::ENOUGH_BALANCE.bits() | Self::NOT_TOO_MUCH_GAS.bits();
44
45        const QUEUED_POOL_BITS  = Self::NO_PARKED_ANCESTORS.bits();
46
47        const BLOB_POOL_BITS  = Self::BLOB_TRANSACTION.bits();
48    }
49}
50
51impl TxState {
52    /// The state of a transaction is considered `pending`, if the transaction has:
53    ///   - _No_ parked ancestors
54    ///   - enough balance
55    ///   - enough fee cap
56    ///   - enough blob fee cap
57    #[inline]
58    pub(crate) const fn is_pending(&self) -> bool {
59        self.bits() >= Self::PENDING_POOL_BITS.bits()
60    }
61
62    /// Whether this transaction is a blob transaction.
63    #[inline]
64    pub(crate) const fn is_blob(&self) -> bool {
65        self.contains(Self::BLOB_TRANSACTION)
66    }
67
68    /// Returns `true` if the transaction has a nonce gap.
69    #[inline]
70    pub(crate) const fn has_nonce_gap(&self) -> bool {
71        !self.intersects(Self::NO_NONCE_GAPS)
72    }
73
74    /// Adds the transaction into the pool.
75    ///
76    /// This pool consists of four sub-pools: `Queued`, `Pending`, `BaseFee`, and `Blob`.
77    ///
78    /// The `Queued` pool contains transactions with gaps in its dependency tree: It requires
79    /// additional transactions that are note yet present in the pool. And transactions that the
80    /// sender can not afford with the current balance.
81    ///
82    /// The `Pending` pool contains all transactions that have no nonce gaps, and can be afforded by
83    /// the sender. It only contains transactions that are ready to be included in the pending
84    /// block. The pending pool contains all transactions that could be listed currently, but not
85    /// necessarily independently. However, this pool never contains transactions with nonce gaps. A
86    /// transaction is considered `ready` when it has the lowest nonce of all transactions from the
87    /// same sender. Which is equals to the chain nonce of the sender in the pending pool.
88    ///
89    /// The `BaseFee` pool contains transactions that currently can't satisfy the dynamic fee
90    /// requirement. With EIP-1559, transactions can become executable or not without any changes to
91    /// the sender's balance or nonce and instead their `feeCap` determines whether the
92    /// transaction is _currently_ (on the current state) ready or needs to be parked until the
93    /// `feeCap` satisfies the block's `baseFee`.
94    ///
95    /// The `Blob` pool contains _blob_ transactions that currently can't satisfy the dynamic fee
96    /// requirement, or blob fee requirement. Transactions become executable only if the
97    /// transaction `feeCap` is greater than the block's `baseFee` and the `maxBlobFee` is greater
98    /// than the block's `blobFee`.
99    ///
100    /// Determines the specific reason why a transaction is queued based on its subpool and state.
101    pub(crate) const fn determine_queued_reason(&self, subpool: SubPool) -> Option<QueuedReason> {
102        match subpool {
103            SubPool::Pending => None, // Not queued
104            SubPool::Queued => {
105                // Check state flags to determine specific reason
106                if !self.contains(Self::NO_NONCE_GAPS) {
107                    Some(QueuedReason::NonceGap)
108                } else if !self.contains(Self::ENOUGH_BALANCE) {
109                    Some(QueuedReason::InsufficientBalance)
110                } else if !self.contains(Self::NO_PARKED_ANCESTORS) {
111                    Some(QueuedReason::ParkedAncestors)
112                } else if !self.contains(Self::NOT_TOO_MUCH_GAS) {
113                    Some(QueuedReason::TooMuchGas)
114                } else {
115                    // Fallback for unexpected queued state
116                    Some(QueuedReason::NonceGap)
117                }
118            }
119            SubPool::BaseFee => Some(QueuedReason::InsufficientBaseFee),
120            SubPool::Blob => {
121                // For blob transactions, derive the queued reason from flags similarly to Queued.
122                if !self.contains(Self::NO_NONCE_GAPS) {
123                    Some(QueuedReason::NonceGap)
124                } else if !self.contains(Self::ENOUGH_BALANCE) {
125                    Some(QueuedReason::InsufficientBalance)
126                } else if !self.contains(Self::NO_PARKED_ANCESTORS) {
127                    Some(QueuedReason::ParkedAncestors)
128                } else if !self.contains(Self::NOT_TOO_MUCH_GAS) {
129                    Some(QueuedReason::TooMuchGas)
130                } else if !self.contains(Self::ENOUGH_FEE_CAP_BLOCK) {
131                    Some(QueuedReason::InsufficientBaseFee)
132                } else if !self.contains(Self::ENOUGH_BLOB_FEE_CAP_BLOCK) {
133                    Some(QueuedReason::InsufficientBlobFee)
134                } else {
135                    // Fallback for unexpected non-pending blob state
136                    Some(QueuedReason::InsufficientBlobFee)
137                }
138            }
139        }
140    }
141}
142
143/// Identifier for the transaction Sub-pool
144#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
145#[repr(u8)]
146pub enum SubPool {
147    /// The queued sub-pool contains transactions that are not ready to be included in the next
148    /// block because they have missing or queued ancestors or the sender the lacks funds to
149    /// execute this transaction.
150    Queued = 0,
151    /// The base-fee sub-pool contains transactions that are not ready to be included in the next
152    /// block because they don't meet the base fee requirement.
153    BaseFee,
154    /// The blob sub-pool contains all blob transactions that are __not__ pending.
155    Blob,
156    /// The pending sub-pool contains transactions that are ready to be included in the next block.
157    Pending,
158}
159
160impl SubPool {
161    /// Whether this transaction is to be moved to the pending sub-pool.
162    #[inline]
163    pub const fn is_pending(&self) -> bool {
164        matches!(self, Self::Pending)
165    }
166
167    /// Whether this transaction is in the queued pool.
168    #[inline]
169    pub const fn is_queued(&self) -> bool {
170        matches!(self, Self::Queued)
171    }
172
173    /// Whether this transaction is in the base fee pool.
174    #[inline]
175    pub const fn is_base_fee(&self) -> bool {
176        matches!(self, Self::BaseFee)
177    }
178
179    /// Whether this transaction is in the blob pool.
180    #[inline]
181    pub const fn is_blob(&self) -> bool {
182        matches!(self, Self::Blob)
183    }
184
185    /// Returns whether this is a promotion depending on the current sub-pool location.
186    #[inline]
187    pub fn is_promoted(&self, other: Self) -> bool {
188        self > &other
189    }
190}
191
192impl From<TxState> for SubPool {
193    fn from(value: TxState) -> Self {
194        if value.is_pending() {
195            Self::Pending
196        } else if value.is_blob() {
197            // all _non-pending_ blob transactions are in the blob sub-pool
198            Self::Blob
199        } else if value.bits() < TxState::BASE_FEE_POOL_BITS.bits() {
200            Self::Queued
201        } else {
202            Self::BaseFee
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_promoted() {
213        assert!(SubPool::BaseFee.is_promoted(SubPool::Queued));
214        assert!(SubPool::Pending.is_promoted(SubPool::BaseFee));
215        assert!(SubPool::Pending.is_promoted(SubPool::Queued));
216        assert!(SubPool::Pending.is_promoted(SubPool::Blob));
217        assert!(!SubPool::BaseFee.is_promoted(SubPool::Pending));
218        assert!(!SubPool::Queued.is_promoted(SubPool::BaseFee));
219    }
220
221    #[test]
222    fn test_tx_state() {
223        let mut state = TxState::default();
224        state |= TxState::NO_NONCE_GAPS;
225        assert!(state.intersects(TxState::NO_NONCE_GAPS))
226    }
227
228    #[test]
229    fn test_tx_queued() {
230        let state = TxState::default();
231        assert_eq!(SubPool::Queued, state.into());
232
233        let state = TxState::NO_PARKED_ANCESTORS |
234            TxState::NO_NONCE_GAPS |
235            TxState::NOT_TOO_MUCH_GAS |
236            TxState::ENOUGH_FEE_CAP_BLOCK;
237        assert_eq!(SubPool::Queued, state.into());
238    }
239
240    #[test]
241    fn test_tx_pending() {
242        let state = TxState::PENDING_POOL_BITS;
243        assert_eq!(SubPool::Pending, state.into());
244        assert!(state.is_pending());
245
246        let bits = 0b11111100;
247        let state = TxState::from_bits(bits).unwrap();
248        assert_eq!(SubPool::Pending, state.into());
249        assert!(state.is_pending());
250
251        let bits = 0b11111110;
252        let state = TxState::from_bits(bits).unwrap();
253        assert_eq!(SubPool::Pending, state.into());
254        assert!(state.is_pending());
255    }
256
257    #[test]
258    fn test_blob() {
259        let mut state = TxState::PENDING_POOL_BITS;
260        state.insert(TxState::BLOB_TRANSACTION);
261        assert!(state.is_pending());
262
263        state.remove(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
264        assert!(state.is_blob());
265        assert!(!state.is_pending());
266
267        state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
268        state.remove(TxState::ENOUGH_FEE_CAP_BLOCK);
269        assert!(state.is_blob());
270        assert!(!state.is_pending());
271    }
272
273    #[test]
274    fn test_tx_state_no_nonce_gap() {
275        let mut state = TxState::default();
276        state |= TxState::NO_NONCE_GAPS;
277        assert!(!state.has_nonce_gap());
278    }
279
280    #[test]
281    fn test_tx_state_with_nonce_gap() {
282        let state = TxState::default();
283        assert!(state.has_nonce_gap());
284    }
285
286    #[test]
287    fn test_tx_state_enough_balance() {
288        let mut state = TxState::default();
289        state.insert(TxState::ENOUGH_BALANCE);
290        assert!(state.contains(TxState::ENOUGH_BALANCE));
291    }
292
293    #[test]
294    fn test_tx_state_not_too_much_gas() {
295        let mut state = TxState::default();
296        state.insert(TxState::NOT_TOO_MUCH_GAS);
297        assert!(state.contains(TxState::NOT_TOO_MUCH_GAS));
298    }
299
300    #[test]
301    fn test_tx_state_enough_fee_cap_block() {
302        let mut state = TxState::default();
303        state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
304        assert!(state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
305    }
306
307    #[test]
308    fn test_tx_base_fee() {
309        let state = TxState::BASE_FEE_POOL_BITS;
310        assert_eq!(SubPool::BaseFee, state.into());
311    }
312
313    #[test]
314    fn test_blob_transaction_only() {
315        let state = TxState::BLOB_TRANSACTION;
316        assert_eq!(SubPool::Blob, state.into());
317        assert!(state.is_blob());
318        assert!(!state.is_pending());
319    }
320
321    #[test]
322    fn test_blob_transaction_with_base_fee_bits() {
323        let mut state = TxState::BASE_FEE_POOL_BITS;
324        state.insert(TxState::BLOB_TRANSACTION);
325        assert_eq!(SubPool::Blob, state.into());
326        assert!(state.is_blob());
327        assert!(!state.is_pending());
328    }
329
330    #[test]
331    fn test_blob_reason_insufficient_base_fee() {
332        // Blob tx with all structural bits set and blob fee sufficient, but base fee insufficient
333        let state = TxState::NO_PARKED_ANCESTORS |
334            TxState::NO_NONCE_GAPS |
335            TxState::ENOUGH_BALANCE |
336            TxState::NOT_TOO_MUCH_GAS |
337            TxState::ENOUGH_BLOB_FEE_CAP_BLOCK |
338            TxState::BLOB_TRANSACTION;
339        // ENOUGH_FEE_CAP_BLOCK intentionally not set
340        let subpool: SubPool = state.into();
341        assert_eq!(subpool, SubPool::Blob);
342        let reason = state.determine_queued_reason(subpool);
343        assert_eq!(reason, Some(QueuedReason::InsufficientBaseFee));
344    }
345
346    #[test]
347    fn test_blob_reason_insufficient_blob_fee() {
348        // Blob tx with all structural bits set and base fee sufficient, but blob fee insufficient
349        let state = TxState::NO_PARKED_ANCESTORS |
350            TxState::NO_NONCE_GAPS |
351            TxState::ENOUGH_BALANCE |
352            TxState::NOT_TOO_MUCH_GAS |
353            TxState::ENOUGH_FEE_CAP_BLOCK |
354            TxState::BLOB_TRANSACTION;
355        // ENOUGH_BLOB_FEE_CAP_BLOCK intentionally not set
356        let subpool: SubPool = state.into();
357        assert_eq!(subpool, SubPool::Blob);
358        let reason = state.determine_queued_reason(subpool);
359        assert_eq!(reason, Some(QueuedReason::InsufficientBlobFee));
360    }
361
362    #[test]
363    fn test_blob_reason_nonce_gap() {
364        // Blob tx with nonce gap should report NonceGap regardless of fee bits
365        let mut state = TxState::NO_PARKED_ANCESTORS |
366            TxState::ENOUGH_BALANCE |
367            TxState::NOT_TOO_MUCH_GAS |
368            TxState::ENOUGH_FEE_CAP_BLOCK |
369            TxState::ENOUGH_BLOB_FEE_CAP_BLOCK |
370            TxState::BLOB_TRANSACTION;
371        state.remove(TxState::NO_NONCE_GAPS);
372        let subpool: SubPool = state.into();
373        assert_eq!(subpool, SubPool::Blob);
374        let reason = state.determine_queued_reason(subpool);
375        assert_eq!(reason, Some(QueuedReason::NonceGap));
376    }
377
378    #[test]
379    fn test_blob_reason_insufficient_balance() {
380        // Blob tx with insufficient balance
381        let state = TxState::NO_PARKED_ANCESTORS |
382            TxState::NO_NONCE_GAPS |
383            TxState::NOT_TOO_MUCH_GAS |
384            TxState::ENOUGH_FEE_CAP_BLOCK |
385            TxState::ENOUGH_BLOB_FEE_CAP_BLOCK |
386            TxState::BLOB_TRANSACTION;
387        // ENOUGH_BALANCE intentionally not set
388        let subpool: SubPool = state.into();
389        assert_eq!(subpool, SubPool::Blob);
390        let reason = state.determine_queued_reason(subpool);
391        assert_eq!(reason, Some(QueuedReason::InsufficientBalance));
392    }
393
394    #[test]
395    fn test_blob_reason_too_much_gas() {
396        // Blob tx exceeding gas limit
397        let mut state = TxState::NO_PARKED_ANCESTORS |
398            TxState::NO_NONCE_GAPS |
399            TxState::ENOUGH_BALANCE |
400            TxState::ENOUGH_FEE_CAP_BLOCK |
401            TxState::ENOUGH_BLOB_FEE_CAP_BLOCK |
402            TxState::BLOB_TRANSACTION;
403        state.remove(TxState::NOT_TOO_MUCH_GAS);
404        let subpool: SubPool = state.into();
405        assert_eq!(subpool, SubPool::Blob);
406        let reason = state.determine_queued_reason(subpool);
407        assert_eq!(reason, Some(QueuedReason::TooMuchGas));
408    }
409}