@@ -14,7 +14,7 @@ use reth_primitives_traits::{
1414use reth_storage_api:: { AccountInfoReader , BlockReaderIdExt , StateProviderFactory } ;
1515use reth_transaction_pool:: {
1616 EthPoolTransaction , EthTransactionValidator , TransactionOrigin , TransactionValidationOutcome ,
17- TransactionValidator , error:: InvalidPoolTransactionError ,
17+ TransactionValidator , error:: InvalidPoolTransactionError , validate :: ValidTransaction ,
1818} ;
1919use std:: sync:: {
2020 Arc ,
@@ -262,7 +262,7 @@ where
262262 if let TransactionValidationOutcome :: Valid {
263263 balance,
264264 state_nonce,
265- transaction : valid_tx,
265+ transaction : mut valid_tx,
266266 propagate,
267267 bytecode_hash,
268268 authorities,
@@ -301,7 +301,18 @@ where
301301 valid_tx. transaction ( ) . gas_limit ( ) ,
302302 ) ) ;
303303
304- let cost = valid_tx. transaction ( ) . cost ( ) . saturating_add ( cost_addition) ;
304+ // Fold the OP fees into `cost()` so the pool's cumulative per-sender balance check
305+ // accounts for them too. This check only sees one transaction against the on-chain
306+ // balance; a sender's later nonces must also cover the OP fees of the earlier ones, or
307+ // the pool marks them pending although execution rejects them for insufficient funds.
308+ // The fees are priced at admission.
309+ match & mut valid_tx {
310+ ValidTransaction :: Valid ( tx) |
311+ ValidTransaction :: ValidWithSidecar { transaction : tx, .. } => {
312+ tx. set_op_fee_reservation ( cost_addition)
313+ }
314+ }
315+ let cost = * valid_tx. transaction ( ) . cost ( ) ;
305316
306317 // Checks for max cost
307318 if cost > balance {
@@ -508,4 +519,90 @@ mod tests {
508519 U256 :: ZERO
509520 ) ;
510521 }
522+
523+ /// The pool must classify a sender's consecutive nonces against the full OP cost, not just the
524+ /// L2 `cost()`. Each tx below is affordable on its own (so both pass admission against the same
525+ /// on-chain balance), but the sender cannot pay the L1 data fee of both. Without the L1 fee in
526+ /// the pool's cumulative accounting, nonce 1 is marked pending and the payload builder retries
527+ /// it every block, failing with insufficient funds.
528+ #[ tokio:: test]
529+ async fn pool_parks_descendant_unaffordable_with_l1_fee ( ) {
530+ use crate :: { OpL1BlockInfo , OpPooledTransaction , OpTransactionValidator } ;
531+ use alloy_consensus:: { SignableTransaction , TxEip1559 , transaction:: Recovered } ;
532+ use alloy_eips:: eip2718:: Encodable2718 ;
533+ use alloy_primitives:: { Address , Signature , TxKind } ;
534+ use parking_lot:: RwLock ;
535+ use reth_optimism_chainspec:: OP_MAINNET ;
536+ use reth_optimism_evm:: { OpEvmConfig , RethL1BlockInfo } ;
537+ use reth_optimism_primitives:: { OpPrimitives , OpTransactionSigned } ;
538+ use reth_provider:: test_utils:: { ExtendedAccount , MockEthProvider } ;
539+ use reth_transaction_pool:: {
540+ CoinbaseTipOrdering , Pool , PoolConfig , PoolTransaction , TransactionOrigin ,
541+ TransactionPool , blobstore:: InMemoryBlobStore ,
542+ validate:: EthTransactionValidatorBuilder ,
543+ } ;
544+ use std:: sync:: atomic:: AtomicU64 ;
545+
546+ let signer = Address :: with_last_byte ( 1 ) ;
547+ let make_tx = |nonce : u64 | -> OpPooledTransaction {
548+ let tx: OpTransactionSigned = TxEip1559 {
549+ chain_id : 10 ,
550+ nonce,
551+ gas_limit : 21_000 ,
552+ max_fee_per_gas : 1_000_000_000 ,
553+ to : TxKind :: Call ( Address :: with_last_byte ( 0x42 ) ) ,
554+ ..Default :: default ( )
555+ }
556+ . into_signed ( Signature :: test_signature ( ) )
557+ . into ( ) ;
558+ let recovered = Recovered :: new_unchecked ( tx, signer) ;
559+ let len = recovered. encode_2718_len ( ) ;
560+ OpPooledTransaction :: new ( recovered, len)
561+ } ;
562+ let ( tx0, tx1) = ( make_tx ( 0 ) , make_tx ( 1 ) ) ;
563+
564+ let mut l1_block_info = L1BlockInfo {
565+ l1_base_fee : U256 :: from ( 1_000_000_000_000u64 ) ,
566+ l1_base_fee_scalar : U256 :: from ( 1_000_000 ) ,
567+ ..Default :: default ( )
568+ } ;
569+ let l1_fee =
570+ l1_block_info. l1_tx_data_fee ( OP_MAINNET . clone ( ) , 0 , tx0. encoded_2718 ( ) , false ) . unwrap ( ) ;
571+ let l2_cost = * tx0. cost ( ) ;
572+ assert ! ( l1_fee > l2_cost, "test needs the L1 fee to dominate, as on ink-mainnet" ) ;
573+
574+ // Covers either tx alone (and both txs' L2 cost), but not both txs' full OP cost.
575+ let balance = l2_cost * U256 :: from ( 2 ) + l1_fee + l1_fee / U256 :: from ( 2 ) ;
576+
577+ let client = MockEthProvider :: < OpPrimitives > :: new ( )
578+ . with_chain_spec ( OP_MAINNET . clone ( ) )
579+ . with_genesis_block ( ) ;
580+ client. add_account ( signer, ExtendedAccount :: new ( 0 , balance) ) ;
581+ let inner =
582+ EthTransactionValidatorBuilder :: new ( client, OpEvmConfig :: optimism ( OP_MAINNET . clone ( ) ) )
583+ . build ( InMemoryBlobStore :: default ( ) ) ;
584+ let validator = OpTransactionValidator :: with_block_info (
585+ inner,
586+ OpL1BlockInfo {
587+ l1_block_info : RwLock :: new ( l1_block_info) ,
588+ timestamp : AtomicU64 :: new ( 0 ) ,
589+ } ,
590+ ) ;
591+ let pool = Pool :: new (
592+ validator,
593+ CoinbaseTipOrdering :: default ( ) ,
594+ InMemoryBlobStore :: default ( ) ,
595+ PoolConfig :: default ( ) ,
596+ ) ;
597+
598+ pool. add_transaction ( TransactionOrigin :: External , tx0) . await . unwrap ( ) ;
599+ pool. add_transaction ( TransactionOrigin :: External , tx1) . await . unwrap ( ) ;
600+
601+ let pending: Vec < _ > =
602+ pool. get_pending_transactions_by_sender ( signer) . iter ( ) . map ( |tx| tx. nonce ( ) ) . collect ( ) ;
603+ let queued: Vec < _ > =
604+ pool. get_queued_transactions_by_sender ( signer) . iter ( ) . map ( |tx| tx. nonce ( ) ) . collect ( ) ;
605+ assert_eq ! ( pending, vec![ 0 ] , "only nonce 0 is executable" ) ;
606+ assert_eq ! ( queued, vec![ 1 ] , "nonce 1 cannot cover its L1 data fee after nonce 0" ) ;
607+ }
511608}
0 commit comments