@@ -24,14 +24,21 @@ use scroll_alloy_hardforks::ScrollHardforks;
2424pub struct ScrollPoolBuilder < T = reth_scroll_txpool:: ScrollPooledTransaction > {
2525 /// Enforced overrides that are applied to the pool config.
2626 pub pool_config_overrides : PoolBuilderConfigOverrides ,
27-
27+ /// Require L1 data fee buffer in balance check.
28+ /// When enabled, validates balance >= `L2_cost` + 2*`L1_cost` but only charges `L2_cost` +
29+ /// 1*`L1_cost`.
30+ pub require_l1_data_fee_buffer : bool ,
2831 /// Marker for the pooled transaction type.
2932 _pd : core:: marker:: PhantomData < T > ,
3033}
3134
3235impl < T > Default for ScrollPoolBuilder < T > {
3336 fn default ( ) -> Self {
34- Self { pool_config_overrides : Default :: default ( ) , _pd : Default :: default ( ) }
37+ Self {
38+ pool_config_overrides : Default :: default ( ) ,
39+ require_l1_data_fee_buffer : false ,
40+ _pd : Default :: default ( ) ,
41+ }
3542 }
3643}
3744
@@ -44,6 +51,14 @@ impl<T> ScrollPoolBuilder<T> {
4451 self . pool_config_overrides = pool_config_overrides;
4552 self
4653 }
54+
55+ /// Sets the require L1 data fee buffer flag.
56+ /// When enabled, validates balance >= `L2_cost` + 2*`L1_cost` but only charges `L2_cost` +
57+ /// 1*`L1_cost`. This matches geth's block validation behavior.
58+ pub const fn with_require_l1_data_fee_buffer ( mut self , require : bool ) -> Self {
59+ self . require_l1_data_fee_buffer = require;
60+ self
61+ }
4762}
4863
4964impl < Node , T > PoolBuilder < Node > for ScrollPoolBuilder < T >
5873 type Pool = ScrollTransactionPool < Node :: Provider , DiskFileBlobStore , T > ;
5974
6075 async fn build_pool ( self , ctx : & BuilderContext < Node > ) -> eyre:: Result < Self :: Pool > {
61- let Self { pool_config_overrides, .. } = self ;
76+ let Self { pool_config_overrides, require_l1_data_fee_buffer , .. } = self ;
6277 let data_dir = ctx. config ( ) . datadir ( ) ;
6378 let blob_store = DiskFileBlobStore :: open ( data_dir. blobstore ( ) , Default :: default ( ) ) ?;
6479
8196 // In --dev mode we can't require gas fees because we're unable to decode
8297 // the L1 block info
8398 . require_l1_data_gas_fee ( !ctx. config ( ) . dev . dev )
99+ . require_l1_data_fee_buffer ( require_l1_data_fee_buffer)
84100 } ) ;
85101
86102 let transaction_pool = reth_transaction_pool:: Pool :: new (
@@ -379,4 +395,111 @@ mod tests {
379395 // drop all validation tasks.
380396 drop ( manager) ;
381397 }
398+
399+ #[ test]
400+ fn test_pool_builder_with_require_l1_data_fee_buffer ( ) {
401+ // Test that the builder method correctly sets the flag
402+ let pool_builder = ScrollPoolBuilder :: < ScrollPooledTransaction > :: default ( ) ;
403+ assert ! ( !pool_builder. require_l1_data_fee_buffer) ;
404+
405+ let pool_builder = pool_builder. with_require_l1_data_fee_buffer ( true ) ;
406+ assert ! ( pool_builder. require_l1_data_fee_buffer) ;
407+
408+ let pool_builder = pool_builder. with_require_l1_data_fee_buffer ( false ) ;
409+ assert ! ( !pool_builder. require_l1_data_fee_buffer) ;
410+ }
411+
412+ #[ tokio:: test]
413+ async fn test_l1_data_fee_buffer_validation ( ) {
414+ // Test that the L1 data fee buffer feature correctly validates transactions:
415+ // - With buffer enabled: rejects when balance < L2_cost + 2*L1_cost
416+ // - With buffer disabled: accepts when balance >= L2_cost + 1*L1_cost
417+ //
418+ // Both scenarios use identical setup to prove only the buffer flag differs.
419+
420+ // Shared test constants
421+ let signer: alloy_primitives:: Address = Default :: default ( ) ;
422+ let balance = U256 :: from ( 500_000_000_000_000u64 ) ; // 500 Twei
423+ let gas_limit = 55_000u64 ;
424+ let gas_price = 7u128 ;
425+ let tx_input = Bytes :: from ( random_iter :: < u8 > ( ) . take ( 100 ) . collect :: < Vec < _ > > ( ) ) ;
426+
427+ // Helper to create a client with identical state
428+ let client =
429+ MockEthProvider :: < ScrollPrimitives , _ > :: new ( ) . with_chain_spec ( SCROLL_DEV . clone ( ) ) ;
430+ let hash = B256 :: random ( ) ;
431+ client. add_header ( hash, Header :: default ( ) ) ;
432+ client. add_block ( hash, ScrollBlock :: default ( ) ) ;
433+ // Balance covers L2_cost + 1*L1_cost but NOT L2_cost + 2*L1_cost
434+ // With u32::MAX storage values, L1 cost is ~483 Twei.
435+ // max L2_cost = 55,000 * 7 = 385,000 Wei.
436+ client. add_account ( signer, ExtendedAccount :: new ( 0 , balance) ) ;
437+ client. add_account (
438+ L1_GAS_PRICE_ORACLE_ADDRESS ,
439+ ExtendedAccount :: new ( 0 , U256 :: ZERO ) . extend_storage (
440+ ( 0u8 ..8 ) . map ( |k| ( B256 :: from ( U256 :: from ( k) ) , U256 :: from ( u32:: MAX ) ) ) ,
441+ ) ,
442+ ) ;
443+
444+ // Helper to create a transaction with identical parameters
445+ let tx = ScrollTxEnvelope :: Legacy ( Signed :: new_unchecked (
446+ TxLegacy { gas_limit, gas_price, input : tx_input. clone ( ) , ..Default :: default ( ) } ,
447+ Signature :: new ( U256 :: ZERO , U256 :: ZERO , false ) ,
448+ Default :: default ( ) ,
449+ ) ) ;
450+ let tx = ScrollPooledTransaction :: new ( Recovered :: new_unchecked ( tx, signer) , 200 ) ;
451+
452+ let handle = tokio:: runtime:: Handle :: current ( ) ;
453+ let manager = TaskManager :: new ( handle) ;
454+
455+ // Test 1: With L1 data fee buffer ENABLED - should reject (requires 2x L1 cost)
456+ let validator = TransactionValidationTaskExecutor :: eth_builder ( client. clone ( ) )
457+ . no_eip4844 ( )
458+ . build_with_tasks ( manager. executor ( ) , NoopBlobStore :: default ( ) )
459+ . map ( |validator| {
460+ ScrollTransactionValidator :: new ( validator)
461+ . require_l1_data_gas_fee ( true )
462+ . require_l1_data_fee_buffer ( true )
463+ } ) ;
464+
465+ let pool = ScrollTransactionPool :: new (
466+ validator,
467+ CoinbaseTipOrdering :: < ScrollPooledTransaction > :: default ( ) ,
468+ NoopBlobStore :: default ( ) ,
469+ PoolConfig :: default ( ) ,
470+ ) ;
471+
472+ let err = pool. add_transaction ( TransactionOrigin :: Local , tx. clone ( ) ) . await . unwrap_err ( ) ;
473+ assert ! ( matches!(
474+ err. kind,
475+ PoolErrorKind :: InvalidTransaction (
476+ InvalidPoolTransactionError :: Consensus ( InvalidTransactionError :: InsufficientFunds ( GotExpectedBoxed ( expected) ) )
477+ ) if * expected == GotExpected { got: balance, expected: U256 :: from( 967347259159872u64 ) }
478+ ) ) ;
479+
480+ // Test 2: With L1 data fee buffer DISABLED - should accept (only requires 1x L1 cost)
481+ let validator = TransactionValidationTaskExecutor :: eth_builder ( client)
482+ . no_eip4844 ( )
483+ . build_with_tasks ( manager. executor ( ) , NoopBlobStore :: default ( ) )
484+ . map ( |validator| {
485+ ScrollTransactionValidator :: new ( validator)
486+ . require_l1_data_gas_fee ( true )
487+ . require_l1_data_fee_buffer ( false )
488+ } ) ;
489+
490+ let pool = ScrollTransactionPool :: new (
491+ validator,
492+ CoinbaseTipOrdering :: < ScrollPooledTransaction > :: default ( ) ,
493+ NoopBlobStore :: default ( ) ,
494+ PoolConfig :: default ( ) ,
495+ ) ;
496+
497+ let result = pool. add_transaction ( TransactionOrigin :: Local , tx) . await ;
498+ assert ! (
499+ result. is_ok( ) ,
500+ "Expected transaction to be accepted without buffer, got: {result:?}"
501+ ) ;
502+
503+ drop ( manager) ;
504+ }
382505}
0 commit comments