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}