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