reth_transaction_pool/pool/
state.rs

1bitflags::bitflags! {
2    /// 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.
3    ///
4    /// This mirrors [erigon's ephemeral state field](https://github.com/ledgerwatch/erigon/wiki/Transaction-Pool-Design#ordering-function).
5    ///
6    /// The [SubPool] the transaction belongs to is derived from its state and determined by the following sequential checks:
7    ///
8    /// - If it satisfies the [TxState::PENDING_POOL_BITS] it belongs in the pending sub-pool: [SubPool::Pending].
9    /// - If it is an EIP-4844 blob transaction it belongs in the blob sub-pool: [SubPool::Blob].
10    /// - If it satisfies the [TxState::BASE_FEE_POOL_BITS] it belongs in the base fee sub-pool: [SubPool::BaseFee].
11    ///
12    /// Otherwise, it belongs in the queued sub-pool: [SubPool::Queued].
13    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
14    pub(crate) struct TxState: u8 {
15        /// Set to `1` if all ancestor transactions are pending.
16        const NO_PARKED_ANCESTORS = 0b10000000;
17        /// Set to `1` of 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.
18        const NO_NONCE_GAPS = 0b01000000;
19        /// Bit derived from the sender's balance.
20        ///
21        /// Set to `1` if the sender's balance can cover the maximum cost for this transaction (`feeCap * gasLimit + value`).
22        /// This includes cumulative costs of prior transactions, which ensures that the sender has enough funds for all max cost of prior transactions.
23        const ENOUGH_BALANCE = 0b00100000;
24        /// Bit set to true if the transaction has a lower gas limit than the block's gas limit.
25        const NOT_TOO_MUCH_GAS = 0b00010000;
26        /// Covers the Dynamic fee requirement.
27        ///
28        /// Set to 1 if `maxFeePerGas` of the transaction meets the requirement of the pending block.
29        const ENOUGH_FEE_CAP_BLOCK = 0b00001000;
30        /// Covers the dynamic blob fee requirement, only relevant for EIP-4844 blob transactions.
31        ///
32        /// Set to 1 if `maxBlobFeePerGas` of the transaction meets the requirement of the pending block.
33        const ENOUGH_BLOB_FEE_CAP_BLOCK = 0b00000100;
34        /// Marks whether the transaction is a blob transaction.
35        ///
36        /// We track this as part of the state for simplicity, since blob transactions are handled differently and are mutually exclusive with normal transactions.
37        const BLOB_TRANSACTION = 0b00000010;
38
39        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();
40
41        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();
42
43        const QUEUED_POOL_BITS  = Self::NO_PARKED_ANCESTORS.bits();
44
45        const BLOB_POOL_BITS  = Self::BLOB_TRANSACTION.bits();
46    }
47}
48
49impl TxState {
50    /// The state of a transaction is considered `pending`, if the transaction has:
51    ///   - _No_ parked ancestors
52    ///   - enough balance
53    ///   - enough fee cap
54    ///   - enough blob fee cap
55    #[inline]
56    pub(crate) const fn is_pending(&self) -> bool {
57        self.bits() >= Self::PENDING_POOL_BITS.bits()
58    }
59
60    /// Whether this transaction is a blob transaction.
61    #[inline]
62    pub(crate) const fn is_blob(&self) -> bool {
63        self.contains(Self::BLOB_TRANSACTION)
64    }
65
66    /// Returns `true` if the transaction has a nonce gap.
67    #[inline]
68    pub(crate) const fn has_nonce_gap(&self) -> bool {
69        !self.intersects(Self::NO_NONCE_GAPS)
70    }
71}
72
73/// Identifier for the transaction Sub-pool
74#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
75#[repr(u8)]
76pub enum SubPool {
77    /// The queued sub-pool contains transactions that are not ready to be included in the next
78    /// block because they have missing or queued ancestors or the sender the lacks funds to
79    /// execute this transaction.
80    Queued = 0,
81    /// The base-fee sub-pool contains transactions that are not ready to be included in the next
82    /// block because they don't meet the base fee requirement.
83    BaseFee,
84    /// The blob sub-pool contains all blob transactions that are __not__ pending.
85    Blob,
86    /// The pending sub-pool contains transactions that are ready to be included in the next block.
87    Pending,
88}
89
90impl SubPool {
91    /// Whether this transaction is to be moved to the pending sub-pool.
92    #[inline]
93    pub const fn is_pending(&self) -> bool {
94        matches!(self, Self::Pending)
95    }
96
97    /// Whether this transaction is in the queued pool.
98    #[inline]
99    pub const fn is_queued(&self) -> bool {
100        matches!(self, Self::Queued)
101    }
102
103    /// Whether this transaction is in the base fee pool.
104    #[inline]
105    pub const fn is_base_fee(&self) -> bool {
106        matches!(self, Self::BaseFee)
107    }
108
109    /// Whether this transaction is in the blob pool.
110    #[inline]
111    pub const fn is_blob(&self) -> bool {
112        matches!(self, Self::Blob)
113    }
114
115    /// Returns whether this is a promotion depending on the current sub-pool location.
116    #[inline]
117    pub fn is_promoted(&self, other: Self) -> bool {
118        self > &other
119    }
120}
121
122impl From<TxState> for SubPool {
123    fn from(value: TxState) -> Self {
124        if value.is_pending() {
125            Self::Pending
126        } else if value.is_blob() {
127            // all _non-pending_ blob transactions are in the blob sub-pool
128            Self::Blob
129        } else if value.bits() < TxState::BASE_FEE_POOL_BITS.bits() {
130            Self::Queued
131        } else {
132            Self::BaseFee
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_promoted() {
143        assert!(SubPool::BaseFee.is_promoted(SubPool::Queued));
144        assert!(SubPool::Pending.is_promoted(SubPool::BaseFee));
145        assert!(SubPool::Pending.is_promoted(SubPool::Queued));
146        assert!(SubPool::Pending.is_promoted(SubPool::Blob));
147        assert!(!SubPool::BaseFee.is_promoted(SubPool::Pending));
148        assert!(!SubPool::Queued.is_promoted(SubPool::BaseFee));
149    }
150
151    #[test]
152    fn test_tx_state() {
153        let mut state = TxState::default();
154        state |= TxState::NO_NONCE_GAPS;
155        assert!(state.intersects(TxState::NO_NONCE_GAPS))
156    }
157
158    #[test]
159    fn test_tx_queued() {
160        let state = TxState::default();
161        assert_eq!(SubPool::Queued, state.into());
162
163        let state = TxState::NO_PARKED_ANCESTORS |
164            TxState::NO_NONCE_GAPS |
165            TxState::NOT_TOO_MUCH_GAS |
166            TxState::ENOUGH_FEE_CAP_BLOCK;
167        assert_eq!(SubPool::Queued, state.into());
168    }
169
170    #[test]
171    fn test_tx_pending() {
172        let state = TxState::PENDING_POOL_BITS;
173        assert_eq!(SubPool::Pending, state.into());
174        assert!(state.is_pending());
175
176        let bits = 0b11111100;
177        let state = TxState::from_bits(bits).unwrap();
178        assert_eq!(SubPool::Pending, state.into());
179        assert!(state.is_pending());
180
181        let bits = 0b11111110;
182        let state = TxState::from_bits(bits).unwrap();
183        assert_eq!(SubPool::Pending, state.into());
184        assert!(state.is_pending());
185    }
186
187    #[test]
188    fn test_blob() {
189        let mut state = TxState::PENDING_POOL_BITS;
190        state.insert(TxState::BLOB_TRANSACTION);
191        assert!(state.is_pending());
192
193        state.remove(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
194        assert!(state.is_blob());
195        assert!(!state.is_pending());
196
197        state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
198        state.remove(TxState::ENOUGH_FEE_CAP_BLOCK);
199        assert!(state.is_blob());
200        assert!(!state.is_pending());
201    }
202
203    #[test]
204    fn test_tx_state_no_nonce_gap() {
205        let mut state = TxState::default();
206        state |= TxState::NO_NONCE_GAPS;
207        assert!(!state.has_nonce_gap());
208    }
209
210    #[test]
211    fn test_tx_state_with_nonce_gap() {
212        let state = TxState::default();
213        assert!(state.has_nonce_gap());
214    }
215
216    #[test]
217    fn test_tx_state_enough_balance() {
218        let mut state = TxState::default();
219        state.insert(TxState::ENOUGH_BALANCE);
220        assert!(state.contains(TxState::ENOUGH_BALANCE));
221    }
222
223    #[test]
224    fn test_tx_state_not_too_much_gas() {
225        let mut state = TxState::default();
226        state.insert(TxState::NOT_TOO_MUCH_GAS);
227        assert!(state.contains(TxState::NOT_TOO_MUCH_GAS));
228    }
229
230    #[test]
231    fn test_tx_state_enough_fee_cap_block() {
232        let mut state = TxState::default();
233        state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
234        assert!(state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
235    }
236
237    #[test]
238    fn test_tx_base_fee() {
239        let state = TxState::BASE_FEE_POOL_BITS;
240        assert_eq!(SubPool::BaseFee, state.into());
241    }
242
243    #[test]
244    fn test_blob_transaction_only() {
245        let state = TxState::BLOB_TRANSACTION;
246        assert_eq!(SubPool::Blob, state.into());
247        assert!(state.is_blob());
248        assert!(!state.is_pending());
249    }
250
251    #[test]
252    fn test_blob_transaction_with_base_fee_bits() {
253        let mut state = TxState::BASE_FEE_POOL_BITS;
254        state.insert(TxState::BLOB_TRANSACTION);
255        assert_eq!(SubPool::Blob, state.into());
256        assert!(state.is_blob());
257        assert!(!state.is_pending());
258    }
259}