Skip to main content

reth_rpc/
testing.rs

1//! Implementation of the `testing` namespace.
2//!
3//! This exposes `testing_buildBlockV1`, intended for non-production/debug use.
4//!
5//! # Enabling the testing namespace
6//!
7//! The `testing_` namespace is disabled by default for security reasons.
8//! To enable it, add `testing` to the `--http.api` flag when starting the node:
9//!
10//! ```sh
11//! reth node --http --http.api eth,testing
12//! ```
13//!
14//! **Warning:** This namespace allows building arbitrary blocks. Never expose it
15//! on public-facing RPC endpoints without proper authentication.
16
17use alloy_consensus::{Header, Transaction};
18use alloy_eips::eip2718::Decodable2718;
19use alloy_evm::{Evm, RecoveredTx};
20use alloy_primitives::{map::HashSet, Address, U256};
21use alloy_rlp::Encodable;
22use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5;
23use async_trait::async_trait;
24use jsonrpsee::core::RpcResult;
25use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
26use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
27use reth_errors::RethError;
28use reth_ethereum_engine_primitives::EthBuiltPayload;
29use reth_ethereum_primitives::EthPrimitives;
30use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
31use reth_primitives_traits::{
32    transaction::{recover::try_recover_signers, signed::RecoveryError},
33    AlloyBlockHeader as BlockTrait, TxTy,
34};
35use reth_revm::{database::StateProviderDatabase, db::State};
36use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
37use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
38use reth_rpc_eth_types::EthApiError;
39use reth_storage_api::{BlockReader, HeaderProvider};
40use revm::context::Block;
41use revm_primitives::map::DefaultHashBuilder;
42use std::sync::Arc;
43use tracing::debug;
44
45/// Testing API handler.
46#[derive(Debug, Clone)]
47pub struct TestingApi<Eth, Evm> {
48    eth_api: Eth,
49    evm_config: Evm,
50    /// If true, skip invalid transactions instead of failing.
51    skip_invalid_transactions: bool,
52    /// If set, override the parent block's gas limit in `testing_buildBlockV1`.
53    gas_limit_override: Option<u64>,
54}
55
56impl<Eth, Evm> TestingApi<Eth, Evm> {
57    /// Create a new testing API handler.
58    pub const fn new(eth_api: Eth, evm_config: Evm) -> Self {
59        Self { eth_api, evm_config, skip_invalid_transactions: false, gas_limit_override: None }
60    }
61
62    /// Enable skipping invalid transactions instead of failing.
63    /// When a transaction fails, all subsequent transactions from the same sender are also
64    /// skipped.
65    pub const fn with_skip_invalid_transactions(mut self) -> Self {
66        self.skip_invalid_transactions = true;
67        self
68    }
69
70    /// Override the gas limit used by `testing_buildBlockV1` instead of inheriting from the
71    /// parent block.
72    pub const fn with_gas_limit_override(mut self, gas_limit: u64) -> Self {
73        self.gas_limit_override = Some(gas_limit);
74        self
75    }
76}
77
78impl<Eth, Evm> TestingApi<Eth, Evm>
79where
80    Eth: Call<
81        Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
82    >,
83    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
84        + 'static,
85{
86    async fn build_block_v1(
87        &self,
88        request: TestingBuildBlockRequestV1,
89    ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
90        let evm_config = self.evm_config.clone();
91        let skip_invalid_transactions = self.skip_invalid_transactions;
92        let gas_limit_override = self.gas_limit_override;
93        self.eth_api
94            .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
95                let state = state.database.0;
96                let mut db = State::builder()
97                    .with_bundle_update()
98                    .with_database(StateProviderDatabase::new(&state))
99                    .build();
100                let parent = eth_api
101                    .provider()
102                    .sealed_header_by_hash(request.parent_block_hash)?
103                    .ok_or_else(|| {
104                    EthApiError::HeaderNotFound(request.parent_block_hash.into())
105                })?;
106
107                let chain_spec = eth_api.provider().chain_spec();
108                let is_osaka =
109                    chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
110
111                let withdrawals = request.payload_attributes.withdrawals.clone();
112                let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);
113
114                let env_attrs = NextBlockEnvAttributes {
115                    timestamp: request.payload_attributes.timestamp,
116                    suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
117                    prev_randao: request.payload_attributes.prev_randao,
118                    gas_limit: gas_limit_override.unwrap_or_else(|| parent.gas_limit()),
119                    parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
120                    withdrawals: withdrawals.map(Into::into),
121                    extra_data: request.extra_data.unwrap_or_default(),
122                    slot_number: request.payload_attributes.slot_number,
123                };
124
125                let mut builder = evm_config
126                    .builder_for_next_block(&mut db, &parent, env_attrs)
127                    .map_err(RethError::other)
128                    .map_err(Eth::Error::from_eth_err)?;
129                builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
130
131                let mut total_fees = U256::ZERO;
132                let base_fee = builder.evm_mut().block().basefee();
133
134                let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
135                let mut block_transactions_rlp_length = 0usize;
136
137                // Decode and recover all transactions in parallel
138                let recovered_txs = try_recover_signers(&request.transactions, |tx| {
139                    TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
140                        .map_err(RecoveryError::from_source)
141                })
142                .or(Err(EthApiError::InvalidTransactionSignature))?;
143
144                for (idx, tx) in recovered_txs.into_iter().enumerate() {
145                    let signer = tx.signer();
146                    if skip_invalid_transactions && invalid_senders.contains(&signer) {
147                        continue;
148                    }
149
150                    // EIP-7934: Check estimated block size before adding transaction
151                    let tx_rlp_len = tx.tx().length();
152                    if is_osaka {
153                        // 1KB overhead for block header
154                        let estimated_block_size = block_transactions_rlp_length +
155                            tx_rlp_len +
156                            withdrawals_rlp_length +
157                            1024;
158                        if estimated_block_size > MAX_RLP_BLOCK_SIZE {
159                            if skip_invalid_transactions {
160                                debug!(
161                                    target: "rpc::testing",
162                                    tx_idx = idx,
163                                    ?signer,
164                                    estimated_block_size,
165                                    max_size = MAX_RLP_BLOCK_SIZE,
166                                    "Skipping transaction: would exceed block size limit"
167                                );
168                                invalid_senders.insert(signer);
169                                continue;
170                            }
171                            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
172                                format!(
173                                    "transaction at index {} would exceed max block size: {} > {}",
174                                    idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
175                                ),
176                            )));
177                        }
178                    }
179
180                    let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
181                    let gas_used = match builder.execute_transaction(tx) {
182                        Ok(gas_used) => gas_used,
183                        Err(err) => {
184                            if skip_invalid_transactions {
185                                debug!(
186                                    target: "rpc::testing",
187                                    tx_idx = idx,
188                                    ?signer,
189                                    error = ?err,
190                                    "Skipping invalid transaction"
191                                );
192                                invalid_senders.insert(signer);
193                                continue;
194                            }
195                            debug!(
196                                target: "rpc::testing",
197                                tx_idx = idx,
198                                ?signer,
199                                error = ?err,
200                                "Transaction execution failed"
201                            );
202                            return Err(Eth::Error::from_eth_err(err));
203                        }
204                    };
205
206                    block_transactions_rlp_length += tx_rlp_len;
207                    total_fees += U256::from(tip) * U256::from(gas_used);
208                }
209                let outcome = builder.finish(&state, None).map_err(Eth::Error::from_eth_err)?;
210
211                let has_requests = outcome.block.requests_hash().is_some();
212                let sealed_block = Arc::new(outcome.block.into_sealed_block());
213
214                let requests = has_requests.then_some(outcome.execution_result.requests);
215
216                EthBuiltPayload::new(sealed_block, total_fees, requests, None)
217                    .try_into_v5()
218                    .map_err(RethError::other)
219                    .map_err(Eth::Error::from_eth_err)
220            })
221            .await
222    }
223}
224
225#[async_trait]
226impl<Eth, Evm> TestingApiServer for TestingApi<Eth, Evm>
227where
228    Eth: Call<
229        Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
230    >,
231    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
232        + 'static,
233{
234    /// Handles `testing_buildBlockV1` by gating concurrency via a semaphore and offloading heavy
235    /// work to the blocking pool to avoid stalling the async runtime.
236    async fn build_block_v1(
237        &self,
238        request: TestingBuildBlockRequestV1,
239    ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
240        self.build_block_v1(request).await.map_err(Into::into)
241    }
242}