@@ -169,7 +169,12 @@ pub async fn validate_tx<T: Transaction>(
169169/// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty 
170170/// - extra_fields must be empty 
171171/// - refunds are not initially supported (refund_percent, refund_recipient, refund_tx_hashes must be empty) 
172- pub  fn  validate_bundle ( bundle :  & EthSendBundle ,  bundle_gas :  u64 )  -> RpcResult < ( ) >  { 
172+ /// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` 
173+ pub  fn  validate_bundle ( 
174+     bundle :  & EthSendBundle , 
175+     bundle_gas :  u64 , 
176+     tx_hashes :  Vec < B256 > , 
177+ )  -> RpcResult < ( ) >  { 
173178    // Don't allow bundles to be submitted over 1 hour into the future 
174179    // TODO: make the window configurable 
175180    let  valid_timestamp_window = SystemTime :: now ( ) 
@@ -225,6 +230,16 @@ pub fn validate_bundle(bundle: &EthSendBundle, bundle_gas: u64) -> RpcResult<()>
225230        ) ; 
226231    } 
227232
233+     // revert protection: all transaction hashes must be in `reverting_tx_hashes` 
234+     for  tx_hash in  & tx_hashes { 
235+         if  !bundle. reverting_tx_hashes . contains ( tx_hash)  { 
236+             return  Err ( EthApiError :: InvalidParams ( 
237+                 "Transaction hash not found in reverting_tx_hashes" . into ( ) , 
238+             ) 
239+             . into_rpc_err ( ) ) ; 
240+         } 
241+     } 
242+ 
228243    Ok ( ( ) ) 
229244} 
230245
@@ -541,7 +556,7 @@ mod tests {
541556            ..Default :: default ( ) 
542557        } ; 
543558        assert_eq ! ( 
544-             validate_bundle( & bundle,  0 ) , 
559+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
545560            Err ( EthApiError :: InvalidParams ( 
546561                "Bundle cannot be more than 1 hour in the future" . into( ) 
547562            ) 
@@ -553,6 +568,7 @@ mod tests {
553568    async  fn  test_err_bundle_max_gas_limit_too_high ( )  { 
554569        let  signer = PrivateKeySigner :: random ( ) ; 
555570        let  mut  encoded_txs = vec ! [ ] ; 
571+         let  mut  tx_hashes = vec ! [ ] ; 
556572
557573        // Create transactions that collectively exceed MAX_BUNDLE_GAS (25M) 
558574        // Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit 
@@ -574,6 +590,8 @@ mod tests {
574590
575591            let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
576592            let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
593+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
594+             tx_hashes. push ( tx_hash) ; 
577595
578596            // Encode the transaction 
579597            let  mut  encoded = vec ! [ ] ; 
@@ -591,7 +609,7 @@ mod tests {
591609        } ; 
592610
593611        // Test should fail due to exceeding gas limit 
594-         let  result = validate_bundle ( & bundle,  total_gas) ; 
612+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes ) ; 
595613        assert ! ( result. is_err( ) ) ; 
596614        if  let  Err ( e)  = result { 
597615            let  error_message = format ! ( "{e:?}" ) ; 
@@ -603,6 +621,7 @@ mod tests {
603621    async  fn  test_err_bundle_too_many_transactions ( )  { 
604622        let  signer = PrivateKeySigner :: random ( ) ; 
605623        let  mut  encoded_txs = vec ! [ ] ; 
624+         let  mut  tx_hashes = vec ! [ ] ; 
606625
607626        let  gas = 4_000_000 ; 
608627        let  mut  total_gas = 0u64 ; 
@@ -622,6 +641,8 @@ mod tests {
622641
623642            let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
624643            let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
644+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
645+             tx_hashes. push ( tx_hash) ; 
625646
626647            // Encode the transaction 
627648            let  mut  encoded = vec ! [ ] ; 
@@ -639,7 +660,7 @@ mod tests {
639660        } ; 
640661
641662        // Test should fail due to exceeding gas limit 
642-         let  result = validate_bundle ( & bundle,  total_gas) ; 
663+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes ) ; 
643664        assert ! ( result. is_err( ) ) ; 
644665        if  let  Err ( e)  = result { 
645666            let  error_message = format ! ( "{e:?}" ) ; 
@@ -655,7 +676,7 @@ mod tests {
655676            ..Default :: default ( ) 
656677        } ; 
657678        assert_eq ! ( 
658-             validate_bundle( & bundle,  0 ) , 
679+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
659680            Err ( 
660681                EthApiError :: InvalidParams ( "Partial transaction dropping is not supported" . into( ) ) 
661682                    . into_rpc_err( ) 
@@ -672,7 +693,7 @@ mod tests {
672693            ..Default :: default ( ) 
673694        } ; 
674695        assert_eq ! ( 
675-             validate_bundle( & bundle,  0 ) , 
696+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
676697            Err ( EthApiError :: InvalidParams ( "extra_fields must be empty" . into( ) ) . into_rpc_err( ) ) 
677698        ) ; 
678699    } 
@@ -684,7 +705,7 @@ mod tests {
684705            ..Default :: default ( ) 
685706        } ; 
686707        assert_eq ! ( 
687-             validate_bundle( & bundle,  0 ) , 
708+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
688709            Err ( 
689710                EthApiError :: InvalidParams ( "refunds are not initially supported" . into( ) ) 
690711                    . into_rpc_err( ) 
@@ -699,7 +720,7 @@ mod tests {
699720            ..Default :: default ( ) 
700721        } ; 
701722        assert_eq ! ( 
702-             validate_bundle( & bundle,  0 ) , 
723+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
703724            Err ( 
704725                EthApiError :: InvalidParams ( "refunds are not initially supported" . into( ) ) 
705726                    . into_rpc_err( ) 
@@ -714,11 +735,62 @@ mod tests {
714735            ..Default :: default ( ) 
715736        } ; 
716737        assert_eq ! ( 
717-             validate_bundle( & bundle,  0 ) , 
738+             validate_bundle( & bundle,  0 ,  vec! [ ] ) , 
718739            Err ( 
719740                EthApiError :: InvalidParams ( "refunds are not initially supported" . into( ) ) 
720741                    . into_rpc_err( ) 
721742            ) 
722743        ) ; 
723744    } 
745+ 
746+     #[ tokio:: test]  
747+     async  fn  test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes ( )  { 
748+         let  signer = PrivateKeySigner :: random ( ) ; 
749+         let  mut  encoded_txs = vec ! [ ] ; 
750+         let  mut  tx_hashes = vec ! [ ] ; 
751+ 
752+         let  gas = 4_000_000 ; 
753+         let  mut  total_gas = 0u64 ; 
754+         for  _ in  0 ..4  { 
755+             let  mut  tx = TxEip1559  { 
756+                 chain_id :  1 , 
757+                 nonce :  0 , 
758+                 gas_limit :  gas, 
759+                 max_fee_per_gas :  200000u128 , 
760+                 max_priority_fee_per_gas :  100000u128 , 
761+                 to :  Address :: random ( ) . into ( ) , 
762+                 value :  U256 :: from ( 1000000u128 ) , 
763+                 access_list :  Default :: default ( ) , 
764+                 input :  bytes ! ( "" ) . clone ( ) , 
765+             } ; 
766+             total_gas = total_gas. saturating_add ( gas) ; 
767+ 
768+             let  signature = signer. sign_transaction_sync ( & mut  tx) . unwrap ( ) ; 
769+             let  envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ; 
770+             let  tx_hash = envelope. clone ( ) . try_into_recovered ( ) . unwrap ( ) . tx_hash ( ) ; 
771+             tx_hashes. push ( tx_hash) ; 
772+ 
773+             // Encode the transaction 
774+             let  mut  encoded = vec ! [ ] ; 
775+             envelope. encode_2718 ( & mut  encoded) ; 
776+             encoded_txs. push ( Bytes :: from ( encoded) ) ; 
777+         } 
778+ 
779+         let  bundle = EthSendBundle  { 
780+             txs :  encoded_txs, 
781+             block_number :  0 , 
782+             min_timestamp :  None , 
783+             max_timestamp :  None , 
784+             reverting_tx_hashes :  tx_hashes[ ..2 ] . to_vec ( ) , 
785+             ..Default :: default ( ) 
786+         } ; 
787+ 
788+         // Test should fail due to exceeding gas limit 
789+         let  result = validate_bundle ( & bundle,  total_gas,  tx_hashes) ; 
790+         assert ! ( result. is_err( ) ) ; 
791+         if  let  Err ( e)  = result { 
792+             let  error_message = format ! ( "{e:?}" ) ; 
793+             assert ! ( error_message. contains( "Bundle can only contain 3 transactions" ) ) ; 
794+         } 
795+     } 
724796} 
0 commit comments