@@ -9,12 +9,12 @@ use op_alloy_network::Optimism;
99use  op_revm:: { OpSpecId ,  l1block:: L1BlockInfo } ; 
1010use  reth_optimism_evm:: extract_l1_info_from_tx; 
1111use  reth_rpc_eth_types:: { EthApiError ,  RpcInvalidTransactionError ,  SignError } ; 
12+ use  std:: collections:: HashSet ; 
1213use  std:: time:: { Duration ,  SystemTime ,  UNIX_EPOCH } ; 
1314use  tips_core:: Bundle ; 
1415use  tracing:: warn; 
1516
16- // TODO: make this configurable 
17- const  MAX_BUNDLE_GAS :  u64  = 30_000_000 ; 
17+ const  MAX_BUNDLE_GAS :  u64  = 25_000_000 ; 
1818
1919/// Account info for a given address 
2020pub  struct  AccountInfo  { 
@@ -166,7 +166,10 @@ pub async fn validate_tx<T: Transaction>(
166166/// Helper function to validate propeties of a bundle. A bundle is valid if it satisfies the following criteria: 
167167/// - The bundle's max_timestamp is not more than 1 hour in the future 
168168/// - The bundle's gas limit is not greater than the maximum allowed gas limit 
169- pub  fn  validate_bundle ( bundle :  & Bundle ,  bundle_gas :  u64 )  -> RpcResult < ( ) >  { 
169+ /// - The bundle can only contain 3 transactions at once 
170+ /// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty 
171+ /// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` 
172+ pub  fn  validate_bundle ( bundle :  & Bundle ,  bundle_gas :  u64 ,  tx_hashes :  Vec < B256 > )  -> RpcResult < ( ) >  { 
170173    // Don't allow bundles to be submitted over 1 hour into the future 
171174    // TODO: make the window configurable 
172175    let  valid_timestamp_window = SystemTime :: now ( ) 
@@ -191,6 +194,33 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> {
191194        ) ; 
192195    } 
193196
197+     // Can only provide 3 transactions at once 
198+     if  bundle. txs . len ( )  > 3  { 
199+         return  Err ( 
200+             EthApiError :: InvalidParams ( "Bundle can only contain 3 transactions" . into ( ) ) 
201+                 . into_rpc_err ( ) , 
202+         ) ; 
203+     } 
204+ 
205+     // Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty 
206+     if  !bundle. dropping_tx_hashes . is_empty ( )  { 
207+         return  Err ( EthApiError :: InvalidParams ( 
208+             "Partial transaction dropping is not supported" . into ( ) , 
209+         ) 
210+         . into_rpc_err ( ) ) ; 
211+     } 
212+ 
213+     // revert protection: all transaction hashes must be in `reverting_tx_hashes` 
214+     let  reverting_tx_hashes_set:  HashSet < _ >  = bundle. reverting_tx_hashes . iter ( ) . collect ( ) ; 
215+     let  tx_hashes_set:  HashSet < _ >  = tx_hashes. iter ( ) . collect ( ) ; 
216+     if  reverting_tx_hashes_set != tx_hashes_set { 
217+         return  Err ( EthApiError :: InvalidParams ( 
218+             "Revert protection is not supported. reverting_tx_hashes must include all hashes" 
219+                 . into ( ) , 
220+         ) 
221+         . into_rpc_err ( ) ) ; 
222+     } 
223+ 
194224    Ok ( ( ) ) 
195225} 
196226
@@ -505,7 +535,7 @@ mod tests {
505535            ..Default :: default ( ) 
506536        } ; 
507537        assert_eq ! ( 
508-             validate_bundle( & bundle,  0 ) , 
538+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
509539            Err ( EthApiError :: InvalidParams ( 
510540                "Bundle cannot be more than 1 hour in the future" . into( ) 
511541            ) 
@@ -517,9 +547,10 @@ mod tests {
517547    async  fn  test_err_bundle_max_gas_limit_too_high ( )  { 
518548        let  signer = PrivateKeySigner :: random ( ) ; 
519549        let  mut  encoded_txs = vec ! [ ] ; 
550+         let  mut  tx_hashes = vec ! [ ] ; 
520551
521-         // Create transactions that collectively exceed MAX_BUNDLE_GAS (30M ) 
522-         // Each transaction uses 4M gas, so 8 transactions = 32M gas > 30M  limit 
552+         // Create transactions that collectively exceed MAX_BUNDLE_GAS (25M ) 
553+         // Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M  limit 
523554        let  gas = 4_000_000 ; 
524555        let  mut  total_gas = 0u64 ; 
525556        for  _ in  0 ..8  { 
@@ -538,6 +569,8 @@ mod tests {
538569
539570            let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
540571            let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
572+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
573+             tx_hashes. push ( tx_hash) ; 
541574
542575            // Encode the transaction 
543576            let  mut  encoded = vec ! [ ] ; 
@@ -555,11 +588,129 @@ mod tests {
555588        } ; 
556589
557590        // Test should fail due to exceeding gas limit 
558-         let  result = validate_bundle ( & bundle,  total_gas) ; 
591+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes ) ; 
559592        assert ! ( result. is_err( ) ) ; 
560593        if  let  Err ( e)  = result { 
561594            let  error_message = format ! ( "{e:?}" ) ; 
562595            assert ! ( error_message. contains( "Bundle gas limit exceeds maximum allowed" ) ) ; 
563596        } 
564597    } 
598+ 
599+     #[ tokio:: test]  
600+     async  fn  test_err_bundle_too_many_transactions ( )  { 
601+         let  signer = PrivateKeySigner :: random ( ) ; 
602+         let  mut  encoded_txs = vec ! [ ] ; 
603+         let  mut  tx_hashes = vec ! [ ] ; 
604+ 
605+         let  gas = 4_000_000 ; 
606+         let  mut  total_gas = 0u64 ; 
607+         for  _ in  0 ..4  { 
608+             let  mut  tx = TxEip1559  { 
609+                 chain_id :  1 , 
610+                 nonce :  0 , 
611+                 gas_limit :  gas, 
612+                 max_fee_per_gas :  200000u128 , 
613+                 max_priority_fee_per_gas :  100000u128 , 
614+                 to :  Address :: random ( ) . into ( ) , 
615+                 value :  U256 :: from ( 1000000u128 ) , 
616+                 access_list :  Default :: default ( ) , 
617+                 input :  bytes ! ( "" ) . clone ( ) , 
618+             } ; 
619+             total_gas = total_gas. saturating_add ( gas) ; 
620+ 
621+             let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
622+             let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
623+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
624+             tx_hashes. push ( tx_hash) ; 
625+ 
626+             // Encode the transaction 
627+             let  mut  encoded = vec ! [ ] ; 
628+             envelope. encode_2718 ( & mut  encoded) ; 
629+             encoded_txs. push ( Bytes :: from ( encoded) ) ; 
630+         } 
631+ 
632+         let  bundle = Bundle  { 
633+             txs :  encoded_txs, 
634+             block_number :  0 , 
635+             min_timestamp :  None , 
636+             max_timestamp :  None , 
637+             reverting_tx_hashes :  vec ! [ ] , 
638+             ..Default :: default ( ) 
639+         } ; 
640+ 
641+         // Test should fail due to exceeding gas limit 
642+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes) ; 
643+         assert ! ( result. is_err( ) ) ; 
644+         if  let  Err ( e)  = result { 
645+             let  error_message = format ! ( "{e:?}" ) ; 
646+             assert ! ( error_message. contains( "Bundle can only contain 3 transactions" ) ) ; 
647+         } 
648+     } 
649+ 
650+     #[ tokio:: test]  
651+     async  fn  test_err_bundle_partial_transaction_dropping_not_supported ( )  { 
652+         let  bundle = Bundle  { 
653+             txs :  vec ! [ ] , 
654+             dropping_tx_hashes :  vec ! [ B256 :: random( ) ] , 
655+             ..Default :: default ( ) 
656+         } ; 
657+         assert_eq ! ( 
658+             validate_bundle( & bundle,  0 ,  vec![ ] ) , 
659+             Err ( 
660+                 EthApiError :: InvalidParams ( "Partial transaction dropping is not supported" . into( ) ) 
661+                     . into_rpc_err( ) 
662+             ) 
663+         ) ; 
664+     } 
665+ 
666+     #[ tokio:: test]  
667+     async  fn  test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes ( )  { 
668+         let  signer = PrivateKeySigner :: random ( ) ; 
669+         let  mut  encoded_txs = vec ! [ ] ; 
670+         let  mut  tx_hashes = vec ! [ ] ; 
671+ 
672+         let  gas = 4_000_000 ; 
673+         let  mut  total_gas = 0u64 ; 
674+         for  _ in  0 ..4  { 
675+             let  mut  tx = TxEip1559  { 
676+                 chain_id :  1 , 
677+                 nonce :  0 , 
678+                 gas_limit :  gas, 
679+                 max_fee_per_gas :  200000u128 , 
680+                 max_priority_fee_per_gas :  100000u128 , 
681+                 to :  Address :: random ( ) . into ( ) , 
682+                 value :  U256 :: from ( 1000000u128 ) , 
683+                 access_list :  Default :: default ( ) , 
684+                 input :  bytes ! ( "" ) . clone ( ) , 
685+             } ; 
686+             total_gas = total_gas. saturating_add ( gas) ; 
687+ 
688+             let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
689+             let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
690+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
691+             tx_hashes. push ( tx_hash) ; 
692+ 
693+             // Encode the transaction 
694+             let  mut  encoded = vec ! [ ] ; 
695+             envelope. encode_2718 ( & mut  encoded) ; 
696+             encoded_txs. push ( Bytes :: from ( encoded) ) ; 
697+         } 
698+ 
699+         let  bundle = Bundle  { 
700+             txs :  encoded_txs, 
701+             block_number :  0 , 
702+             min_timestamp :  None , 
703+             max_timestamp :  None , 
704+             reverting_tx_hashes :  tx_hashes[ ..2 ] . to_vec ( ) , 
705+             ..Default :: default ( ) 
706+         } ; 
707+ 
708+         // Test should fail due to exceeding gas limit 
709+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes) ; 
710+         assert ! ( result. is_err( ) ) ; 
711+         if  let  Err ( e)  = result { 
712+             let  error_message = format ! ( "{e:?}" ) ; 
713+             assert ! ( error_message. contains( "Bundle can only contain 3 transactions" ) ) ; 
714+         } 
715+     } 
565716} 
0 commit comments