diff --git a/crates/ethereum/node/src/node.rs b/crates/ethereum/node/src/node.rs index cf409cce9c..95ff807253 100644 --- a/crates/ethereum/node/src/node.rs +++ b/crates/ethereum/node/src/node.rs @@ -303,6 +303,8 @@ where let eth_config = EthConfigHandler::new(ctx.node.provider().clone(), ctx.node.evm_config().clone()); + let testing_skip_invalid_transactions = ctx.config.rpc.testing_skip_invalid_transactions; + self.inner .launch_add_ons_with(ctx, move |container| { container.modules.merge_if_module_configured( @@ -316,14 +318,16 @@ where // testing_buildBlockV1: only wire when the hidden testing module is explicitly // requested on any transport. Default stays disabled to honor security guidance. - let testing_api = TestingApi::new( + let mut testing_api = TestingApi::new( container.registry.eth_api().clone(), container.registry.evm_config().clone(), - ) - .into_rpc(); + ); + if testing_skip_invalid_transactions { + testing_api = testing_api.with_skip_invalid_transactions(); + } container .modules - .merge_if_module_configured(RethRpcModule::Testing, testing_api)?; + .merge_if_module_configured(RethRpcModule::Testing, testing_api.into_rpc())?; Ok(()) }) diff --git a/crates/node/core/src/args/rpc_server.rs b/crates/node/core/src/args/rpc_server.rs index 7c2253b7b0..0b0dcc066a 100644 --- a/crates/node/core/src/args/rpc_server.rs +++ b/crates/node/core/src/args/rpc_server.rs @@ -640,6 +640,13 @@ pub struct RpcServerArgs { value_parser = parse_duration_from_secs_or_ms, )] pub rpc_send_raw_transaction_sync_timeout: Duration, + + /// Skip invalid transactions in `testing_buildBlockV1` instead of failing. + /// + /// When enabled, transactions that fail execution will be skipped, and all subsequent + /// transactions from the same sender will also be skipped. + #[arg(long = "testing.skip-invalid-transactions", default_value_t = false)] + pub testing_skip_invalid_transactions: bool, } impl RpcServerArgs { @@ -852,6 +859,7 @@ impl Default for RpcServerArgs { rpc_state_cache, gas_price_oracle, rpc_send_raw_transaction_sync_timeout, + testing_skip_invalid_transactions: false, } } } @@ -1026,6 +1034,7 @@ mod tests { default_suggested_fee: None, }, rpc_send_raw_transaction_sync_timeout: std::time::Duration::from_secs(30), + testing_skip_invalid_transactions: true, }; let parsed_args = CommandParser::::parse_from([ @@ -1114,6 +1123,7 @@ mod tests { "60", "--rpc.send-raw-transaction-sync-timeout", "30s", + "--testing.skip-invalid-transactions", ]) .args; diff --git a/crates/rpc/rpc/src/testing.rs b/crates/rpc/rpc/src/testing.rs index 833f0749e2..c1c8a65d1c 100644 --- a/crates/rpc/rpc/src/testing.rs +++ b/crates/rpc/rpc/src/testing.rs @@ -4,7 +4,7 @@ use alloy_consensus::{Header, Transaction}; use alloy_evm::Evm; -use alloy_primitives::U256; +use alloy_primitives::{map::HashSet, Address, U256}; use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5; use async_trait::async_trait; use jsonrpsee::core::RpcResult; @@ -19,19 +19,31 @@ use reth_rpc_eth_api::{helpers::Call, FromEthApiError}; use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError}; use reth_storage_api::{BlockReader, HeaderProvider}; use revm::context::Block; +use revm_primitives::map::DefaultHashBuilder; use std::sync::Arc; +use tracing::debug; /// Testing API handler. #[derive(Debug, Clone)] pub struct TestingApi { eth_api: Eth, evm_config: Evm, + /// If true, skip invalid transactions instead of failing. + skip_invalid_transactions: bool, } impl TestingApi { /// Create a new testing API handler. pub const fn new(eth_api: Eth, evm_config: Evm) -> Self { - Self { eth_api, evm_config } + Self { eth_api, evm_config, skip_invalid_transactions: false } + } + + /// Enable skipping invalid transactions instead of failing. + /// When a transaction fails, all subsequent transactions from the same sender are also + /// skipped. + pub const fn with_skip_invalid_transactions(mut self) -> Self { + self.skip_invalid_transactions = true; + self } } @@ -46,6 +58,7 @@ where request: TestingBuildBlockRequestV1, ) -> Result { let evm_config = self.evm_config.clone(); + let skip_invalid_transactions = self.skip_invalid_transactions; self.eth_api .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| { let state = state.database.0; @@ -79,11 +92,33 @@ where let mut total_fees = U256::ZERO; let base_fee = builder.evm_mut().block().basefee(); + let mut invalid_senders: HashSet = HashSet::default(); + for tx in request.transactions { let tx: Recovered> = recover_raw_transaction(&tx)?; + let sender = tx.signer(); + + if skip_invalid_transactions && invalid_senders.contains(&sender) { + continue; + } + let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default(); - let gas_used = - builder.execute_transaction(tx).map_err(Eth::Error::from_eth_err)?; + let gas_used = match builder.execute_transaction(tx) { + Ok(gas_used) => gas_used, + Err(err) => { + if skip_invalid_transactions { + debug!( + target: "rpc::testing", + ?sender, + error = ?err, + "Skipping invalid transaction" + ); + invalid_senders.insert(sender); + continue; + } + return Err(Eth::Error::from_eth_err(err)); + } + }; total_fees += U256::from(tip) * U256::from(gas_used); } diff --git a/docs/vocs/docs/pages/cli/op-reth/node.mdx b/docs/vocs/docs/pages/cli/op-reth/node.mdx index 74964bf641..f245315040 100644 --- a/docs/vocs/docs/pages/cli/op-reth/node.mdx +++ b/docs/vocs/docs/pages/cli/op-reth/node.mdx @@ -530,6 +530,11 @@ Gas Price Oracle: [default: 30s] + --testing.skip-invalid-transactions + Skip invalid transactions in `testing_buildBlockV1` instead of failing. + + When enabled, transactions that fail execution will be skipped, and all subsequent transactions from the same sender will also be skipped. + TxPool: --txpool.pending-max-count Max number of transaction in the pending sub-pool diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index d6be9ba55e..c052076fc8 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -530,6 +530,11 @@ Gas Price Oracle: [default: 30s] + --testing.skip-invalid-transactions + Skip invalid transactions in `testing_buildBlockV1` instead of failing. + + When enabled, transactions that fail execution will be skipped, and all subsequent transactions from the same sender will also be skipped. + TxPool: --txpool.pending-max-count Max number of transaction in the pending sub-pool