@@ -2,15 +2,20 @@ use alloy_consensus::private::alloy_eips::{BlockId, BlockNumberOrTag};
22use alloy_consensus:: { Transaction , Typed2718 , constants:: KECCAK_EMPTY , transaction:: Recovered } ;
33use alloy_primitives:: { Address , B256 , U256 } ;
44use alloy_provider:: { Provider , RootProvider } ;
5+ use alloy_rpc_types_mev:: EthSendBundle ;
56use async_trait:: async_trait;
67use jsonrpsee:: core:: RpcResult ;
78use op_alloy_consensus:: interop:: CROSS_L2_INBOX_ADDRESS ;
89use op_alloy_network:: Optimism ;
910use op_revm:: { OpSpecId , l1block:: L1BlockInfo } ;
1011use reth_optimism_evm:: extract_l1_info_from_tx;
1112use reth_rpc_eth_types:: { EthApiError , RpcInvalidTransactionError , SignError } ;
13+ use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
1214use tracing:: warn;
1315
16+ // TODO: make this configurable
17+ const MAX_BUNDLE_GAS : u64 = 30_000_000 ;
18+
1419/// Account info for a given address
1520pub struct AccountInfo {
1621 pub balance : U256 ,
@@ -158,17 +163,51 @@ pub async fn validate_tx<T: Transaction>(
158163 Ok ( ( ) )
159164}
160165
166+ /// Helper function to validate propeties of a bundle. A bundle is valid if it satisfies the following criteria:
167+ /// - The bundle's max_timestamp is not more than 1 hour in the future
168+ /// - The bundle's gas limit is not greater than the maximum allowed gas limit
169+ pub fn validate_bundle ( bundle : & EthSendBundle , bundle_gas : u64 ) -> RpcResult < ( ) > {
170+ // Don't allow bundles to be submitted over 1 hour into the future
171+ // TODO: make the window configurable
172+ let valid_timestamp_window = SystemTime :: now ( )
173+ . duration_since ( UNIX_EPOCH )
174+ . unwrap ( )
175+ . as_secs ( )
176+ + Duration :: from_secs ( 3600 ) . as_secs ( ) ;
177+ if let Some ( max_timestamp) = bundle. max_timestamp
178+ && max_timestamp > valid_timestamp_window
179+ {
180+ return Err ( EthApiError :: InvalidParams (
181+ "Bundle cannot be more than 1 hour in the future" . into ( ) ,
182+ )
183+ . into_rpc_err ( ) ) ;
184+ }
185+
186+ // Check max gas limit for the entire bundle
187+ if bundle_gas > MAX_BUNDLE_GAS {
188+ return Err (
189+ EthApiError :: InvalidParams ( "Bundle gas limit exceeds maximum allowed" . into ( ) )
190+ . into_rpc_err ( ) ,
191+ ) ;
192+ }
193+
194+ Ok ( ( ) )
195+ }
196+
161197#[ cfg( test) ]
162198mod tests {
163199 use super :: * ;
164200 use alloy_consensus:: SignableTransaction ;
165201 use alloy_consensus:: { Transaction , constants:: KECCAK_EMPTY , transaction:: SignerRecoverable } ;
166202 use alloy_consensus:: { TxEip1559 , TxEip4844 , TxEip7702 } ;
203+ use alloy_primitives:: Bytes ;
167204 use alloy_primitives:: { bytes, keccak256} ;
168205 use alloy_signer_local:: PrivateKeySigner ;
169206 use op_alloy_consensus:: OpTxEnvelope ;
170207 use op_alloy_network:: TxSignerSync ;
208+ use op_alloy_network:: eip2718:: Encodable2718 ;
171209 use revm_context_interface:: transaction:: { AccessList , AccessListItem } ;
210+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
172211
173212 fn create_account ( nonce : u64 , balance : U256 ) -> AccountInfo {
174213 AccountInfo {
@@ -452,4 +491,75 @@ mod tests {
452491 . into_rpc_err( ) )
453492 ) ;
454493 }
494+
495+ #[ tokio:: test]
496+ async fn test_err_bundle_max_timestamp_too_far_in_the_future ( ) {
497+ let current_time = SystemTime :: now ( )
498+ . duration_since ( UNIX_EPOCH )
499+ . unwrap ( )
500+ . as_secs ( ) ;
501+ let too_far_in_the_future = current_time + 3601 ;
502+ let bundle = EthSendBundle {
503+ txs : vec ! [ ] ,
504+ max_timestamp : Some ( too_far_in_the_future) ,
505+ ..Default :: default ( )
506+ } ;
507+ assert_eq ! (
508+ validate_bundle( & bundle, 0 ) ,
509+ Err ( EthApiError :: InvalidParams (
510+ "Bundle cannot be more than 1 hour in the future" . into( )
511+ )
512+ . into_rpc_err( ) )
513+ ) ;
514+ }
515+
516+ #[ tokio:: test]
517+ async fn test_err_bundle_max_gas_limit_too_high ( ) {
518+ let signer = PrivateKeySigner :: random ( ) ;
519+ let mut encoded_txs = vec ! [ ] ;
520+
521+ // Create transactions that collectively exceed MAX_BUNDLE_GAS (30M)
522+ // Each transaction uses 4M gas, so 8 transactions = 32M gas > 30M limit
523+ let gas = 4_000_000 ;
524+ let mut total_gas = 0u64 ;
525+ for _ in 0 ..8 {
526+ let mut tx = TxEip1559 {
527+ chain_id : 1 ,
528+ nonce : 0 ,
529+ gas_limit : gas,
530+ max_fee_per_gas : 200000u128 ,
531+ max_priority_fee_per_gas : 100000u128 ,
532+ to : Address :: random ( ) . into ( ) ,
533+ value : U256 :: from ( 1000000u128 ) ,
534+ access_list : Default :: default ( ) ,
535+ input : bytes ! ( "" ) . clone ( ) ,
536+ } ;
537+ total_gas = total_gas. saturating_add ( gas) ;
538+
539+ let signature = signer. sign_transaction_sync ( & mut tx) . unwrap ( ) ;
540+ let envelope = OpTxEnvelope :: Eip1559 ( tx. into_signed ( signature) ) ;
541+
542+ // Encode the transaction
543+ let mut encoded = vec ! [ ] ;
544+ envelope. encode_2718 ( & mut encoded) ;
545+ encoded_txs. push ( Bytes :: from ( encoded) ) ;
546+ }
547+
548+ let bundle = EthSendBundle {
549+ txs : encoded_txs,
550+ block_number : 0 ,
551+ min_timestamp : None ,
552+ max_timestamp : None ,
553+ reverting_tx_hashes : vec ! [ ] ,
554+ ..Default :: default ( )
555+ } ;
556+
557+ // Test should fail due to exceeding gas limit
558+ let result = validate_bundle ( & bundle, total_gas) ;
559+ assert ! ( result. is_err( ) ) ;
560+ if let Err ( e) = result {
561+ let error_message = format ! ( "{e:?}" ) ;
562+ assert ! ( error_message. contains( "Bundle gas limit exceeds maximum allowed" ) ) ;
563+ }
564+ }
455565}
0 commit comments