reth_transaction_pool/pool/
state.rs

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

        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();

        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();

        const QUEUED_POOL_BITS  = Self::NO_PARKED_ANCESTORS.bits();

        const BLOB_POOL_BITS  = Self::BLOB_TRANSACTION.bits();
    }
}

impl TxState {
    /// The state of a transaction is considered `pending`, if the transaction has:
    ///   - _No_ parked ancestors
    ///   - enough balance
    ///   - enough fee cap
    ///   - enough blob fee cap
    #[inline]
    pub(crate) const fn is_pending(&self) -> bool {
        self.bits() >= Self::PENDING_POOL_BITS.bits()
    }

    /// Whether this transaction is a blob transaction.
    #[inline]
    pub(crate) const fn is_blob(&self) -> bool {
        self.contains(Self::BLOB_TRANSACTION)
    }

    /// Returns `true` if the transaction has a nonce gap.
    #[inline]
    pub(crate) const fn has_nonce_gap(&self) -> bool {
        !self.intersects(Self::NO_NONCE_GAPS)
    }
}

/// Identifier for the transaction Sub-pool
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(u8)]
pub enum SubPool {
    /// The queued sub-pool contains transactions that are not ready to be included in the next
    /// block because they have missing or queued ancestors or the sender the lacks funds to
    /// execute this transaction.
    Queued = 0,
    /// The base-fee sub-pool contains transactions that are not ready to be included in the next
    /// block because they don't meet the base fee requirement.
    BaseFee,
    /// The blob sub-pool contains all blob transactions that are __not__ pending.
    Blob,
    /// The pending sub-pool contains transactions that are ready to be included in the next block.
    Pending,
}

impl SubPool {
    /// Whether this transaction is to be moved to the pending sub-pool.
    #[inline]
    pub const fn is_pending(&self) -> bool {
        matches!(self, Self::Pending)
    }

    /// Whether this transaction is in the queued pool.
    #[inline]
    pub const fn is_queued(&self) -> bool {
        matches!(self, Self::Queued)
    }

    /// Whether this transaction is in the base fee pool.
    #[inline]
    pub const fn is_base_fee(&self) -> bool {
        matches!(self, Self::BaseFee)
    }

    /// Whether this transaction is in the blob pool.
    #[inline]
    pub const fn is_blob(&self) -> bool {
        matches!(self, Self::Blob)
    }

    /// Returns whether this is a promotion depending on the current sub-pool location.
    #[inline]
    pub fn is_promoted(&self, other: Self) -> bool {
        self > &other
    }
}

impl From<TxState> for SubPool {
    fn from(value: TxState) -> Self {
        if value.is_pending() {
            Self::Pending
        } else if value.is_blob() {
            // all _non-pending_ blob transactions are in the blob sub-pool
            Self::Blob
        } else if value.bits() < TxState::BASE_FEE_POOL_BITS.bits() {
            Self::Queued
        } else {
            Self::BaseFee
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_promoted() {
        assert!(SubPool::BaseFee.is_promoted(SubPool::Queued));
        assert!(SubPool::Pending.is_promoted(SubPool::BaseFee));
        assert!(SubPool::Pending.is_promoted(SubPool::Queued));
        assert!(SubPool::Pending.is_promoted(SubPool::Blob));
        assert!(!SubPool::BaseFee.is_promoted(SubPool::Pending));
        assert!(!SubPool::Queued.is_promoted(SubPool::BaseFee));
    }

    #[test]
    fn test_tx_state() {
        let mut state = TxState::default();
        state |= TxState::NO_NONCE_GAPS;
        assert!(state.intersects(TxState::NO_NONCE_GAPS))
    }

    #[test]
    fn test_tx_queued() {
        let state = TxState::default();
        assert_eq!(SubPool::Queued, state.into());

        let state = TxState::NO_PARKED_ANCESTORS |
            TxState::NO_NONCE_GAPS |
            TxState::NOT_TOO_MUCH_GAS |
            TxState::ENOUGH_FEE_CAP_BLOCK;
        assert_eq!(SubPool::Queued, state.into());
    }

    #[test]
    fn test_tx_pending() {
        let state = TxState::PENDING_POOL_BITS;
        assert_eq!(SubPool::Pending, state.into());
        assert!(state.is_pending());

        let bits = 0b11111100;
        let state = TxState::from_bits(bits).unwrap();
        assert_eq!(SubPool::Pending, state.into());
        assert!(state.is_pending());

        let bits = 0b11111110;
        let state = TxState::from_bits(bits).unwrap();
        assert_eq!(SubPool::Pending, state.into());
        assert!(state.is_pending());
    }

    #[test]
    fn test_blob() {
        let mut state = TxState::PENDING_POOL_BITS;
        state.insert(TxState::BLOB_TRANSACTION);
        assert!(state.is_pending());

        state.remove(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
        assert!(state.is_blob());
        assert!(!state.is_pending());

        state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
        state.remove(TxState::ENOUGH_FEE_CAP_BLOCK);
        assert!(state.is_blob());
        assert!(!state.is_pending());
    }

    #[test]
    fn test_tx_state_no_nonce_gap() {
        let mut state = TxState::default();
        state |= TxState::NO_NONCE_GAPS;
        assert!(!state.has_nonce_gap());
    }

    #[test]
    fn test_tx_state_with_nonce_gap() {
        let state = TxState::default();
        assert!(state.has_nonce_gap());
    }

    #[test]
    fn test_tx_state_enough_balance() {
        let mut state = TxState::default();
        state.insert(TxState::ENOUGH_BALANCE);
        assert!(state.contains(TxState::ENOUGH_BALANCE));
    }

    #[test]
    fn test_tx_state_not_too_much_gas() {
        let mut state = TxState::default();
        state.insert(TxState::NOT_TOO_MUCH_GAS);
        assert!(state.contains(TxState::NOT_TOO_MUCH_GAS));
    }

    #[test]
    fn test_tx_state_enough_fee_cap_block() {
        let mut state = TxState::default();
        state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
        assert!(state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
    }

    #[test]
    fn test_tx_base_fee() {
        let state = TxState::BASE_FEE_POOL_BITS;
        assert_eq!(SubPool::BaseFee, state.into());
    }

    #[test]
    fn test_blob_transaction_only() {
        let state = TxState::BLOB_TRANSACTION;
        assert_eq!(SubPool::Blob, state.into());
        assert!(state.is_blob());
        assert!(!state.is_pending());
    }

    #[test]
    fn test_blob_transaction_with_base_fee_bits() {
        let mut state = TxState::BASE_FEE_POOL_BITS;
        state.insert(TxState::BLOB_TRANSACTION);
        assert_eq!(SubPool::Blob, state.into());
        assert!(state.is_blob());
        assert!(!state.is_pending());
    }
}