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::{eip1559::calculate_block_gas_limit, eip2718::Decodable2718};
19use alloy_evm::{Evm, RecoveredTx};
20use alloy_primitives::{
21    map::{DefaultHashBuilder, HashSet},
22    Address, Bytes, B256, U256,
23};
24use alloy_rlp::Encodable;
25use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV5, ForkchoiceState, PayloadAttributes};
26use async_trait::async_trait;
27use jsonrpsee::core::RpcResult;
28use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
29use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
30use reth_engine_primitives::ConsensusEngineHandle;
31use reth_errors::RethError;
32use reth_ethereum_engine_primitives::EthBuiltPayload;
33use reth_ethereum_primitives::EthPrimitives;
34use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
35use reth_payload_primitives::PayloadTypes;
36use reth_primitives_traits::{
37    transaction::{recover::try_recover_signers, signed::RecoveryError},
38    AlloyBlockHeader as BlockTrait, TxTy,
39};
40use reth_revm::{database::StateProviderDatabase, db::State};
41use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
42use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
43use reth_rpc_eth_types::EthApiError;
44use reth_storage_api::{BlockReader, BlockReaderIdExt, HeaderProvider};
45use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
46use revm::context::Block;
47use std::sync::Arc;
48use tracing::debug;
49
50/// Testing API handler.
51#[derive(Debug, Clone)]
52pub struct TestingApi<
53    Eth,
54    Evm,
55    Payload: PayloadTypes = reth_ethereum_engine_primitives::EthEngineTypes,
56> {
57    eth_api: Eth,
58    evm_config: Evm,
59    /// Desired gas limit to move toward while respecting the consensus gas limit bounds.
60    desired_gas_limit: u64,
61    engine_handle: ConsensusEngineHandle<Payload>,
62    /// If true, skip invalid transactions instead of failing.
63    skip_invalid_transactions: bool,
64    /// If set, override the block gas limit in `testing_buildBlockV1`.
65    gas_limit_override: Option<u64>,
66}
67
68impl<Eth, Evm, Payload: PayloadTypes> TestingApi<Eth, Evm, Payload> {
69    /// Create a new testing API handler.
70    pub const fn new(
71        eth_api: Eth,
72        evm_config: Evm,
73        desired_gas_limit: u64,
74        engine_handle: ConsensusEngineHandle<Payload>,
75    ) -> Self {
76        Self {
77            eth_api,
78            evm_config,
79            desired_gas_limit,
80            engine_handle,
81            skip_invalid_transactions: false,
82            gas_limit_override: None,
83        }
84    }
85
86    /// Enable skipping invalid transactions instead of failing.
87    /// When a transaction fails, all subsequent transactions from the same sender are also
88    /// skipped.
89    pub const fn with_skip_invalid_transactions(mut self) -> Self {
90        self.skip_invalid_transactions = true;
91        self
92    }
93
94    /// Override the gas limit used by `testing_buildBlockV1`.
95    pub const fn with_gas_limit_override(mut self, gas_limit: u64) -> Self {
96        self.gas_limit_override = Some(gas_limit);
97        self
98    }
99}
100
101impl<Eth, Evm, Payload> TestingApi<Eth, Evm, Payload>
102where
103    Payload: PayloadTypes,
104    Payload::ExecutionData: From<EthBuiltPayload>,
105    Eth: Call<
106        Provider: BlockReader<Header = Header>
107                      + BlockReaderIdExt<Header = Header>
108                      + ChainSpecProvider<ChainSpec: EthereumHardforks>,
109        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
110    >,
111    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
112        + 'static,
113{
114    async fn build_payload_v1(
115        &self,
116        request: TestingBuildBlockRequestV1,
117        skip_invalid_transactions: bool,
118        use_pool_transactions: bool,
119    ) -> Result<EthBuiltPayload, Eth::Error> {
120        let evm_config = self.evm_config.clone();
121        let desired_gas_limit = self.desired_gas_limit;
122        let gas_limit_override = self.gas_limit_override;
123        self.eth_api
124            .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
125                let state = state.database.0;
126                let parent = eth_api
127                    .provider()
128                    .sealed_header_by_hash(request.parent_block_hash)?
129                    .ok_or_else(|| {
130                    EthApiError::HeaderNotFound(request.parent_block_hash.into())
131                })?;
132
133                let chain_spec = eth_api.provider().chain_spec();
134                let is_amsterdam = chain_spec
135                    .is_amsterdam_active_at_timestamp(request.payload_attributes.timestamp);
136                let is_osaka =
137                    chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
138                let mut db = State::builder()
139                    .with_bundle_update()
140                    .with_database(StateProviderDatabase::new(&state))
141                    .with_bal_builder_if(is_amsterdam)
142                    .build();
143
144                let withdrawals = request.payload_attributes.withdrawals.clone();
145                let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);
146
147                let env_attrs = NextBlockEnvAttributes {
148                    timestamp: request.payload_attributes.timestamp,
149                    suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
150                    prev_randao: request.payload_attributes.prev_randao,
151                    gas_limit: gas_limit_override.unwrap_or_else(|| {
152                        calculate_block_gas_limit(parent.gas_limit(), desired_gas_limit)
153                    }),
154                    parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
155                    withdrawals: withdrawals.map(Into::into),
156                    extra_data: request.extra_data.unwrap_or_default(),
157                    slot_number: request.payload_attributes.slot_number,
158                };
159
160                let mut builder = evm_config
161                    .builder_for_next_block(&mut db, &parent, env_attrs)
162                    .map_err(RethError::other)
163                    .map_err(Eth::Error::from_eth_err)?;
164                builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
165
166                let mut total_fees = U256::ZERO;
167                let base_fee = builder.evm_mut().block().basefee();
168
169                let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
170                let mut block_transactions_rlp_length = 0usize;
171
172                // If no transactions are provided in the request, use transactions from the pool.
173                let recovered_txs = if use_pool_transactions {
174                    let mut best_txs = eth_api.pool().best_transactions_with_attributes(
175                        BestTransactionsAttributes::new(
176                            base_fee,
177                            builder
178                                .evm_mut()
179                                .block()
180                                .blob_gasprice()
181                                .map(|gasprice| gasprice as u64),
182                        ),
183                    );
184                    best_txs.no_updates();
185                    best_txs.map(|tx| tx.to_consensus()).collect()
186                } else {
187                    // Decode and recover all transactions in parallel
188                    try_recover_signers(&request.transactions, |tx| {
189                        TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
190                            .map_err(RecoveryError::from_source)
191                    })
192                    .or(Err(EthApiError::InvalidTransactionSignature))?
193                };
194                let allow_skip_invalid_transactions =
195                    skip_invalid_transactions || use_pool_transactions;
196
197                for (idx, tx) in recovered_txs.into_iter().enumerate() {
198                    let signer = tx.signer();
199                    if allow_skip_invalid_transactions && invalid_senders.contains(&signer) {
200                        continue;
201                    }
202
203                    // EIP-7934: Check estimated block size before adding transaction
204                    let tx_rlp_len = tx.tx().length();
205                    if is_osaka {
206                        // 1KB overhead for block header
207                        let estimated_block_size = block_transactions_rlp_length +
208                            tx_rlp_len +
209                            withdrawals_rlp_length +
210                            1024;
211                        if estimated_block_size > MAX_RLP_BLOCK_SIZE {
212                            if allow_skip_invalid_transactions {
213                                debug!(
214                                    target: "rpc::testing",
215                                    tx_idx = idx,
216                                    ?signer,
217                                    estimated_block_size,
218                                    max_size = MAX_RLP_BLOCK_SIZE,
219                                    "Skipping transaction: would exceed block size limit"
220                                );
221                                invalid_senders.insert(signer);
222                                continue;
223                            }
224                            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
225                                format!(
226                                    "transaction at index {} would exceed max block size: {} > {}",
227                                    idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
228                                ),
229                            )));
230                        }
231                    }
232
233                    let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
234                    let gas_used = match builder.execute_transaction(tx) {
235                        Ok(gas_used) => gas_used.tx_gas_used(),
236                        Err(err) => {
237                            if allow_skip_invalid_transactions {
238                                debug!(
239                                    target: "rpc::testing",
240                                    tx_idx = idx,
241                                    ?signer,
242                                    error = ?err,
243                                    "Skipping invalid transaction"
244                                );
245                                invalid_senders.insert(signer);
246                                continue;
247                            }
248                            debug!(
249                                target: "rpc::testing",
250                                tx_idx = idx,
251                                ?signer,
252                                error = ?err,
253                                "Transaction execution failed"
254                            );
255                            return Err(Eth::Error::from_eth_err(err));
256                        }
257                    };
258
259                    block_transactions_rlp_length += tx_rlp_len;
260                    total_fees += U256::from(tip) * U256::from(gas_used);
261                }
262                let outcome = builder.finish(&state, None).map_err(Eth::Error::from_eth_err)?;
263
264                let has_requests = outcome.block.requests_hash().is_some();
265                let requests = has_requests.then_some(outcome.execution_result.requests);
266                let block_access_list = outcome
267                    .block_access_list
268                    .map(|block_access_list| alloy_rlp::encode(&block_access_list).into());
269
270                Ok(EthBuiltPayload::new(
271                    Arc::new(outcome.block),
272                    total_fees,
273                    requests,
274                    block_access_list,
275                ))
276            })
277            .await
278    }
279
280    async fn build_block_v1(
281        &self,
282        request: TestingBuildBlockRequestV1,
283        use_pool_transactions: bool,
284    ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
285        self.build_payload_v1(request, self.skip_invalid_transactions, use_pool_transactions)
286            .await?
287            .try_into_v5()
288            .map_err(RethError::other)
289            .map_err(Eth::Error::from_eth_err)
290    }
291
292    async fn commit_block_v1(
293        &self,
294        payload_attributes: PayloadAttributes,
295        transactions: Option<Vec<Bytes>>,
296        extra_data: Option<Bytes>,
297    ) -> Result<B256, Eth::Error> {
298        let parent = self
299            .eth_api
300            .provider()
301            .latest_header()
302            .map_err(EthApiError::from)?
303            .ok_or_else(|| EthApiError::HeaderNotFound(alloy_eips::BlockId::latest()))?;
304        let safe_block_hash = self
305            .eth_api
306            .provider()
307            .safe_header()
308            .map_err(EthApiError::from)?
309            .map(|header| header.hash())
310            .unwrap_or_else(|| parent.hash());
311        let finalized_block_hash = self
312            .eth_api
313            .provider()
314            .finalized_header()
315            .map_err(EthApiError::from)?
316            .map(|header| header.hash())
317            .unwrap_or_else(|| parent.hash());
318
319        let use_pool_transactions = transactions.is_none();
320        let payload = self
321            .build_payload_v1(
322                TestingBuildBlockRequestV1 {
323                    parent_block_hash: parent.hash(),
324                    payload_attributes,
325                    transactions: transactions.unwrap_or_default(),
326                    extra_data,
327                },
328                false,
329                use_pool_transactions,
330            )
331            .await?;
332
333        let block_hash = payload.block().hash();
334        let execution_data: Payload::ExecutionData = payload.into();
335        let status = self
336            .engine_handle
337            .new_payload(execution_data)
338            .await
339            .map_err(RethError::other)
340            .map_err(Eth::Error::from_eth_err)?;
341        if !status.is_valid() {
342            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
343                "new payload returned non-valid status: {:?}",
344                status.status
345            ))));
346        }
347
348        let fcu = self
349            .engine_handle
350            .fork_choice_updated(
351                ForkchoiceState {
352                    head_block_hash: block_hash,
353                    safe_block_hash,
354                    finalized_block_hash,
355                },
356                None,
357            )
358            .await
359            .map_err(RethError::other)
360            .map_err(Eth::Error::from_eth_err)?;
361        if !fcu.is_valid() {
362            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
363                "forkchoice update returned non-valid status: {:?}",
364                fcu.payload_status.status
365            ))));
366        }
367
368        Ok(block_hash)
369    }
370}
371
372#[async_trait]
373impl<Eth, Evm, Payload> TestingApiServer for TestingApi<Eth, Evm, Payload>
374where
375    Payload: PayloadTypes,
376    Payload::ExecutionData: From<EthBuiltPayload>,
377    Eth: Call<
378        Provider: BlockReader<Header = Header>
379                      + BlockReaderIdExt<Header = Header>
380                      + ChainSpecProvider<ChainSpec: EthereumHardforks>,
381        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
382    >,
383    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
384        + 'static,
385{
386    /// Handles `testing_buildBlockV1` by gating concurrency via a semaphore and offloading heavy
387    /// work to the blocking pool to avoid stalling the async runtime.
388    async fn build_block_v1(
389        &self,
390        parent_block_hash: B256,
391        payload_attributes: PayloadAttributes,
392        transactions: Option<Vec<Bytes>>,
393        extra_data: Option<Bytes>,
394    ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
395        let use_pool_transactions = transactions.is_none();
396        let request = TestingBuildBlockRequestV1 {
397            parent_block_hash,
398            payload_attributes,
399            transactions: transactions.unwrap_or_default(),
400            extra_data,
401        };
402        self.build_block_v1(request, use_pool_transactions).await.map_err(Into::into)
403    }
404
405    /// Handles `testing_commitBlockV1` by building on the current canonical head, then submitting
406    /// the payload and advancing forkchoice through the same engine handle used by the Engine API.
407    async fn commit_block_v1(
408        &self,
409        payload_attributes: PayloadAttributes,
410        transactions: Option<Vec<Bytes>>,
411        extra_data: Option<Bytes>,
412    ) -> RpcResult<B256> {
413        self.commit_block_v1(payload_attributes, transactions, extra_data).await.map_err(Into::into)
414    }
415}