@@ -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