@@ -40,7 +40,7 @@ use std::{
40
40
ops:: Bound :: { Excluded , Unbounded } ,
41
41
sync:: Arc ,
42
42
} ;
43
- use tracing:: trace;
43
+ use tracing:: { trace, warn } ;
44
44
45
45
#[ cfg_attr( doc, aquamarine:: aquamarine) ]
46
46
// TODO: Inlined diagram due to a bug in aquamarine library, should become an include when it's
@@ -293,19 +293,40 @@ impl<T: TransactionOrdering> TxPool<T> {
293
293
Ordering :: Greater
294
294
}
295
295
Ordering :: Less => {
296
- // decreased base fee: recheck basefee pool and promote all that are now valid
297
- let removed =
298
- self . basefee_pool . enforce_basefee ( self . all_transactions . pending_fees . base_fee ) ;
299
- for tx in removed {
300
- let to = {
301
- let tx =
296
+ // Base fee decreased: recheck BaseFee and promote.
297
+ // Invariants:
298
+ // - BaseFee contains only non-blob txs (blob txs live in Blob) and they already
299
+ // have ENOUGH_BLOB_FEE_CAP_BLOCK.
300
+ // - PENDING_POOL_BITS = BASE_FEE_POOL_BITS | ENOUGH_FEE_CAP_BLOCK |
301
+ // ENOUGH_BLOB_FEE_CAP_BLOCK.
302
+ // With the lower base fee they gain ENOUGH_FEE_CAP_BLOCK, so we can set the bit and
303
+ // insert directly into Pending (skip generic routing).
304
+ self . basefee_pool . enforce_basefee_with (
305
+ self . all_transactions . pending_fees . base_fee ,
306
+ |tx| {
307
+ // Update transaction state — guaranteed Pending by the invariants above
308
+ let meta =
302
309
self . all_transactions . txs . get_mut ( tx. id ( ) ) . expect ( "tx exists in set" ) ;
303
- tx. state . insert ( TxState :: ENOUGH_FEE_CAP_BLOCK ) ;
304
- tx. subpool = tx. state . into ( ) ;
305
- tx. subpool
306
- } ;
307
- self . add_transaction_to_subpool ( to, tx) ;
308
- }
310
+ meta. state . insert ( TxState :: ENOUGH_FEE_CAP_BLOCK ) ;
311
+ meta. subpool = meta. state . into ( ) ;
312
+
313
+ trace ! ( target: "txpool" , hash=%tx. transaction. hash( ) , pool=?meta. subpool, "Adding transaction to a subpool" ) ;
314
+ match meta. subpool {
315
+ SubPool :: Queued => self . queued_pool . add_transaction ( tx) ,
316
+ SubPool :: Pending => {
317
+ self . pending_pool . add_transaction ( tx, self . all_transactions . pending_fees . base_fee ) ;
318
+ }
319
+ SubPool :: Blob => {
320
+ self . blob_pool . add_transaction ( tx) ;
321
+ }
322
+ SubPool :: BaseFee => {
323
+ // This should be unreachable as transactions from BaseFee pool with
324
+ // decreased basefee are guaranteed to become Pending
325
+ warn ! ( target: "txpool" , "BaseFee transactions should become Pending after basefee decrease" ) ;
326
+ }
327
+ }
328
+ } ,
329
+ ) ;
309
330
310
331
Ordering :: Less
311
332
}
@@ -2954,6 +2975,97 @@ mod tests {
2954
2975
assert_eq ! ( pool. all_transactions. txs. get( & id) . unwrap( ) . subpool, SubPool :: BaseFee )
2955
2976
}
2956
2977
2978
+ #[ test]
2979
+ fn basefee_decrease_promotes_affordable_and_keeps_unaffordable ( ) {
2980
+ use alloy_primitives:: address;
2981
+ let mut f = MockTransactionFactory :: default ( ) ;
2982
+ let mut pool = TxPool :: new ( MockOrdering :: default ( ) , Default :: default ( ) ) ;
2983
+
2984
+ // Create transactions that will be in basefee pool (can't afford initial high fee)
2985
+ // Use different senders to avoid nonce gap issues
2986
+ let sender_a = address ! ( "0x000000000000000000000000000000000000000a" ) ;
2987
+ let sender_b = address ! ( "0x000000000000000000000000000000000000000b" ) ;
2988
+ let sender_c = address ! ( "0x000000000000000000000000000000000000000c" ) ;
2989
+
2990
+ let tx1 = MockTransaction :: eip1559 ( )
2991
+ . set_sender ( sender_a)
2992
+ . set_nonce ( 0 )
2993
+ . set_max_fee ( 500 )
2994
+ . inc_limit ( ) ;
2995
+ let tx2 = MockTransaction :: eip1559 ( )
2996
+ . set_sender ( sender_b)
2997
+ . set_nonce ( 0 )
2998
+ . set_max_fee ( 600 )
2999
+ . inc_limit ( ) ;
3000
+ let tx3 = MockTransaction :: eip1559 ( )
3001
+ . set_sender ( sender_c)
3002
+ . set_nonce ( 0 )
3003
+ . set_max_fee ( 400 )
3004
+ . inc_limit ( ) ;
3005
+
3006
+ // Set high initial basefee so transactions go to basefee pool
3007
+ let mut block_info = pool. block_info ( ) ;
3008
+ block_info. pending_basefee = 700 ;
3009
+ pool. set_block_info ( block_info) ;
3010
+
3011
+ let validated1 = f. validated ( tx1) ;
3012
+ let validated2 = f. validated ( tx2) ;
3013
+ let validated3 = f. validated ( tx3) ;
3014
+ let id1 = * validated1. id ( ) ;
3015
+ let id2 = * validated2. id ( ) ;
3016
+ let id3 = * validated3. id ( ) ;
3017
+
3018
+ // Add transactions - they should go to basefee pool due to high basefee
3019
+ // All transactions have nonce 0 from different senders, so on_chain_nonce should be 0 for
3020
+ // all
3021
+ pool. add_transaction ( validated1, U256 :: from ( 10_000 ) , 0 , None ) . unwrap ( ) ;
3022
+ pool. add_transaction ( validated2, U256 :: from ( 10_000 ) , 0 , None ) . unwrap ( ) ;
3023
+ pool. add_transaction ( validated3, U256 :: from ( 10_000 ) , 0 , None ) . unwrap ( ) ;
3024
+
3025
+ // Debug: Check where transactions ended up
3026
+ println ! ( "Basefee pool len: {}" , pool. basefee_pool. len( ) ) ;
3027
+ println ! ( "Pending pool len: {}" , pool. pending_pool. len( ) ) ;
3028
+ println ! ( "tx1 subpool: {:?}" , pool. all_transactions. txs. get( & id1) . unwrap( ) . subpool) ;
3029
+ println ! ( "tx2 subpool: {:?}" , pool. all_transactions. txs. get( & id2) . unwrap( ) . subpool) ;
3030
+ println ! ( "tx3 subpool: {:?}" , pool. all_transactions. txs. get( & id3) . unwrap( ) . subpool) ;
3031
+
3032
+ // Verify they're in basefee pool
3033
+ assert_eq ! ( pool. basefee_pool. len( ) , 3 ) ;
3034
+ assert_eq ! ( pool. pending_pool. len( ) , 0 ) ;
3035
+ assert_eq ! ( pool. all_transactions. txs. get( & id1) . unwrap( ) . subpool, SubPool :: BaseFee ) ;
3036
+ assert_eq ! ( pool. all_transactions. txs. get( & id2) . unwrap( ) . subpool, SubPool :: BaseFee ) ;
3037
+ assert_eq ! ( pool. all_transactions. txs. get( & id3) . unwrap( ) . subpool, SubPool :: BaseFee ) ;
3038
+
3039
+ // Now decrease basefee to trigger the zero-allocation optimization
3040
+ let mut block_info = pool. block_info ( ) ;
3041
+ block_info. pending_basefee = 450 ; // tx1 (500) and tx2 (600) can now afford it, tx3 (400) cannot
3042
+ pool. set_block_info ( block_info) ;
3043
+
3044
+ // Verify the optimization worked correctly:
3045
+ // - tx1 and tx2 should be promoted to pending (mathematical certainty)
3046
+ // - tx3 should remain in basefee pool
3047
+ // - All state transitions should be correct
3048
+ assert_eq ! ( pool. basefee_pool. len( ) , 1 ) ;
3049
+ assert_eq ! ( pool. pending_pool. len( ) , 2 ) ;
3050
+
3051
+ // tx3 should still be in basefee pool (fee 400 < basefee 450)
3052
+ assert_eq ! ( pool. all_transactions. txs. get( & id3) . unwrap( ) . subpool, SubPool :: BaseFee ) ;
3053
+
3054
+ // tx1 and tx2 should be in pending pool with correct state bits
3055
+ let tx1_meta = pool. all_transactions . txs . get ( & id1) . unwrap ( ) ;
3056
+ let tx2_meta = pool. all_transactions . txs . get ( & id2) . unwrap ( ) ;
3057
+ assert_eq ! ( tx1_meta. subpool, SubPool :: Pending ) ;
3058
+ assert_eq ! ( tx2_meta. subpool, SubPool :: Pending ) ;
3059
+ assert ! ( tx1_meta. state. contains( TxState :: ENOUGH_FEE_CAP_BLOCK ) ) ;
3060
+ assert ! ( tx2_meta. state. contains( TxState :: ENOUGH_FEE_CAP_BLOCK ) ) ;
3061
+
3062
+ // Verify that best_transactions returns the promoted transactions
3063
+ let best: Vec < _ > = pool. best_transactions ( ) . take ( 3 ) . collect ( ) ;
3064
+ assert_eq ! ( best. len( ) , 2 ) ; // Only tx1 and tx2 should be returned
3065
+ assert ! ( best. iter( ) . any( |tx| tx. id( ) == & id1) ) ;
3066
+ assert ! ( best. iter( ) . any( |tx| tx. id( ) == & id2) ) ;
3067
+ }
3068
+
2957
3069
#[ test]
2958
3070
fn get_highest_transaction_by_sender_and_nonce ( ) {
2959
3071
// Set up a mock transaction factory and a new transaction pool.
0 commit comments