feat(rpc): add flag to skip invalid transactions in testing_buildBlockV1 (#21094)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Dan Cline
2026-01-15 12:05:30 +00:00
committed by GitHub
parent 9bcd3712c8
commit d469b7f1d0
5 changed files with 67 additions and 8 deletions

View File

@@ -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(())
})

View File

@@ -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::<RpcServerArgs>::parse_from([
@@ -1114,6 +1123,7 @@ mod tests {
"60",
"--rpc.send-raw-transaction-sync-timeout",
"30s",
"--testing.skip-invalid-transactions",
])
.args;

View File

@@ -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, Evm> {
eth_api: Eth,
evm_config: Evm,
/// If true, skip invalid transactions instead of failing.
skip_invalid_transactions: bool,
}
impl<Eth, Evm> TestingApi<Eth, Evm> {
/// 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<ExecutionPayloadEnvelopeV5, Eth::Error> {
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<Address, DefaultHashBuilder> = HashSet::default();
for tx in request.transactions {
let tx: Recovered<TxTy<Evm::Primitives>> = 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);
}

View File

@@ -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 <PENDING_MAX_COUNT>
Max number of transaction in the pending sub-pool

View File

@@ -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 <PENDING_MAX_COUNT>
Max number of transaction in the pending sub-pool