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