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 => Some(QueuedReason::InsufficientBlobFee),
121        }
122    }
123}
124
125/// Identifier for the transaction Sub-pool
126#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
127#[repr(u8)]
128pub enum SubPool {
129    /// The queued sub-pool contains transactions that are not ready to be included in the next
130    /// block because they have missing or queued ancestors or the sender the lacks funds to
131    /// execute this transaction.
132    Queued = 0,
133    /// The base-fee sub-pool contains transactions that are not ready to be included in the next
134    /// block because they don't meet the base fee requirement.
135    BaseFee,
136    /// The blob sub-pool contains all blob transactions that are __not__ pending.
137    Blob,
138    /// The pending sub-pool contains transactions that are ready to be included in the next block.
139    Pending,
140}
141
142impl SubPool {
143    /// Whether this transaction is to be moved to the pending sub-pool.
144    #[inline]
145    pub const fn is_pending(&self) -> bool {
146        matches!(self, Self::Pending)
147    }
148
149    /// Whether this transaction is in the queued pool.
150    #[inline]
151    pub const fn is_queued(&self) -> bool {
152        matches!(self, Self::Queued)
153    }
154
155    /// Whether this transaction is in the base fee pool.
156    #[inline]
157    pub const fn is_base_fee(&self) -> bool {
158        matches!(self, Self::BaseFee)
159    }
160
161    /// Whether this transaction is in the blob pool.
162    #[inline]
163    pub const fn is_blob(&self) -> bool {
164        matches!(self, Self::Blob)
165    }
166
167    /// Returns whether this is a promotion depending on the current sub-pool location.
168    #[inline]
169    pub fn is_promoted(&self, other: Self) -> bool {
170        self > &other
171    }
172}
173
174impl From<TxState> for SubPool {
175    fn from(value: TxState) -> Self {
176        if value.is_pending() {
177            Self::Pending
178        } else if value.is_blob() {
179            // all _non-pending_ blob transactions are in the blob sub-pool
180            Self::Blob
181        } else if value.bits() < TxState::BASE_FEE_POOL_BITS.bits() {
182            Self::Queued
183        } else {
184            Self::BaseFee
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_promoted() {
195        assert!(SubPool::BaseFee.is_promoted(SubPool::Queued));
196        assert!(SubPool::Pending.is_promoted(SubPool::BaseFee));
197        assert!(SubPool::Pending.is_promoted(SubPool::Queued));
198        assert!(SubPool::Pending.is_promoted(SubPool::Blob));
199        assert!(!SubPool::BaseFee.is_promoted(SubPool::Pending));
200        assert!(!SubPool::Queued.is_promoted(SubPool::BaseFee));
201    }
202
203    #[test]
204    fn test_tx_state() {
205        let mut state = TxState::default();
206        state |= TxState::NO_NONCE_GAPS;
207        assert!(state.intersects(TxState::NO_NONCE_GAPS))
208    }
209
210    #[test]
211    fn test_tx_queued() {
212        let state = TxState::default();
213        assert_eq!(SubPool::Queued, state.into());
214
215        let state = TxState::NO_PARKED_ANCESTORS |
216            TxState::NO_NONCE_GAPS |
217            TxState::NOT_TOO_MUCH_GAS |
218            TxState::ENOUGH_FEE_CAP_BLOCK;
219        assert_eq!(SubPool::Queued, state.into());
220    }
221
222    #[test]
223    fn test_tx_pending() {
224        let state = TxState::PENDING_POOL_BITS;
225        assert_eq!(SubPool::Pending, state.into());
226        assert!(state.is_pending());
227
228        let bits = 0b11111100;
229        let state = TxState::from_bits(bits).unwrap();
230        assert_eq!(SubPool::Pending, state.into());
231        assert!(state.is_pending());
232
233        let bits = 0b11111110;
234        let state = TxState::from_bits(bits).unwrap();
235        assert_eq!(SubPool::Pending, state.into());
236        assert!(state.is_pending());
237    }
238
239    #[test]
240    fn test_blob() {
241        let mut state = TxState::PENDING_POOL_BITS;
242        state.insert(TxState::BLOB_TRANSACTION);
243        assert!(state.is_pending());
244
245        state.remove(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
246        assert!(state.is_blob());
247        assert!(!state.is_pending());
248
249        state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
250        state.remove(TxState::ENOUGH_FEE_CAP_BLOCK);
251        assert!(state.is_blob());
252        assert!(!state.is_pending());
253    }
254
255    #[test]
256    fn test_tx_state_no_nonce_gap() {
257        let mut state = TxState::default();
258        state |= TxState::NO_NONCE_GAPS;
259        assert!(!state.has_nonce_gap());
260    }
261
262    #[test]
263    fn test_tx_state_with_nonce_gap() {
264        let state = TxState::default();
265        assert!(state.has_nonce_gap());
266    }
267
268    #[test]
269    fn test_tx_state_enough_balance() {
270        let mut state = TxState::default();
271        state.insert(TxState::ENOUGH_BALANCE);
272        assert!(state.contains(TxState::ENOUGH_BALANCE));
273    }
274
275    #[test]
276    fn test_tx_state_not_too_much_gas() {
277        let mut state = TxState::default();
278        state.insert(TxState::NOT_TOO_MUCH_GAS);
279        assert!(state.contains(TxState::NOT_TOO_MUCH_GAS));
280    }
281
282    #[test]
283    fn test_tx_state_enough_fee_cap_block() {
284        let mut state = TxState::default();
285        state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
286        assert!(state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
287    }
288
289    #[test]
290    fn test_tx_base_fee() {
291        let state = TxState::BASE_FEE_POOL_BITS;
292        assert_eq!(SubPool::BaseFee, state.into());
293    }
294
295    #[test]
296    fn test_blob_transaction_only() {
297        let state = TxState::BLOB_TRANSACTION;
298        assert_eq!(SubPool::Blob, state.into());
299        assert!(state.is_blob());
300        assert!(!state.is_pending());
301    }
302
303    #[test]
304    fn test_blob_transaction_with_base_fee_bits() {
305        let mut state = TxState::BASE_FEE_POOL_BITS;
306        state.insert(TxState::BLOB_TRANSACTION);
307        assert_eq!(SubPool::Blob, state.into());
308        assert!(state.is_blob());
309        assert!(!state.is_pending());
310    }
311}