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;
20use alloy_primitives::{map::HashSet, Address, U256};
21use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5;
22use async_trait::async_trait;
23use jsonrpsee::core::RpcResult;
24use reth_errors::RethError;
25use reth_ethereum_engine_primitives::EthBuiltPayload;
26use reth_ethereum_primitives::EthPrimitives;
27use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
28use reth_primitives_traits::{
29    transaction::{recover::try_recover_signers, signed::RecoveryError},
30    AlloyBlockHeader as BlockTrait, TxTy,
31};
32use reth_revm::{database::StateProviderDatabase, db::State};
33use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
34use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
35use reth_rpc_eth_types::EthApiError;
36use reth_storage_api::{BlockReader, HeaderProvider};
37use revm::context::Block;
38use revm_primitives::map::DefaultHashBuilder;
39use std::sync::Arc;
40use tracing::debug;
41
42/// Testing API handler.
43#[derive(Debug, Clone)]
44pub struct TestingApi<Eth, Evm> {
45    eth_api: Eth,
46    evm_config: Evm,
47    /// If true, skip invalid transactions instead of failing.
48    skip_invalid_transactions: bool,
49}
50
51impl<Eth, Evm> TestingApi<Eth, Evm> {
52    /// Create a new testing API handler.
53    pub const fn new(eth_api: Eth, evm_config: Evm) -> Self {
54        Self { eth_api, evm_config, skip_invalid_transactions: false }
55    }
56
57    /// Enable skipping invalid transactions instead of failing.
58    /// When a transaction fails, all subsequent transactions from the same sender are also
59    /// skipped.
60    pub const fn with_skip_invalid_transactions(mut self) -> Self {
61        self.skip_invalid_transactions = true;
62        self
63    }
64}
65
66impl<Eth, Evm> TestingApi<Eth, Evm>
67where
68    Eth: Call<Provider: BlockReader<Header = Header>>,
69    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
70        + 'static,
71{
72    async fn build_block_v1(
73        &self,
74        request: TestingBuildBlockRequestV1,
75    ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
76        let evm_config = self.evm_config.clone();
77        let skip_invalid_transactions = self.skip_invalid_transactions;
78        self.eth_api
79            .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
80                let state = state.database.0;
81                let mut db = State::builder()
82                    .with_bundle_update()
83                    .with_database(StateProviderDatabase::new(&state))
84                    .build();
85                let parent = eth_api
86                    .provider()
87                    .sealed_header_by_hash(request.parent_block_hash)?
88                    .ok_or_else(|| {
89                    EthApiError::HeaderNotFound(request.parent_block_hash.into())
90                })?;
91
92                let env_attrs = NextBlockEnvAttributes {
93                    timestamp: request.payload_attributes.timestamp,
94                    suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
95                    prev_randao: request.payload_attributes.prev_randao,
96                    gas_limit: parent.gas_limit(),
97                    parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
98                    withdrawals: request.payload_attributes.withdrawals.map(Into::into),
99                    extra_data: request.extra_data.unwrap_or_default(),
100                };
101
102                let mut builder = evm_config
103                    .builder_for_next_block(&mut db, &parent, env_attrs)
104                    .map_err(RethError::other)
105                    .map_err(Eth::Error::from_eth_err)?;
106                builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
107
108                let mut total_fees = U256::ZERO;
109                let base_fee = builder.evm_mut().block().basefee();
110
111                let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
112
113                // Decode and recover all transactions in parallel
114                let recovered_txs = try_recover_signers(&request.transactions, |tx| {
115                    TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
116                        .map_err(RecoveryError::from_source)
117                })
118                .or(Err(EthApiError::InvalidTransactionSignature))?;
119
120                for (idx, tx) in recovered_txs.into_iter().enumerate() {
121                    let signer = tx.signer();
122                    if skip_invalid_transactions && invalid_senders.contains(&signer) {
123                        continue;
124                    }
125
126                    let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
127                    let gas_used = match builder.execute_transaction(tx) {
128                        Ok(gas_used) => gas_used,
129                        Err(err) => {
130                            if skip_invalid_transactions {
131                                debug!(
132                                    target: "rpc::testing",
133                                    tx_idx = idx,
134                                    ?signer,
135                                    error = ?err,
136                                    "Skipping invalid transaction"
137                                );
138                                invalid_senders.insert(signer);
139                                continue;
140                            }
141                            debug!(
142                                target: "rpc::testing",
143                                tx_idx = idx,
144                                ?signer,
145                                error = ?err,
146                                "Transaction execution failed"
147                            );
148                            return Err(Eth::Error::from_eth_err(err));
149                        }
150                    };
151
152                    total_fees += U256::from(tip) * U256::from(gas_used);
153                }
154                let outcome = builder.finish(&state).map_err(Eth::Error::from_eth_err)?;
155
156                let requests = outcome
157                    .block
158                    .requests_hash()
159                    .is_some()
160                    .then_some(outcome.execution_result.requests);
161
162                EthBuiltPayload::new(
163                    alloy_rpc_types_engine::PayloadId::default(),
164                    Arc::new(outcome.block.into_sealed_block()),
165                    total_fees,
166                    requests,
167                )
168                .try_into_v5()
169                .map_err(RethError::other)
170                .map_err(Eth::Error::from_eth_err)
171            })
172            .await
173    }
174}
175
176#[async_trait]
177impl<Eth, Evm> TestingApiServer for TestingApi<Eth, Evm>
178where
179    Eth: Call<Provider: BlockReader<Header = Header>>,
180    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
181        + 'static,
182{
183    /// Handles `testing_buildBlockV1` by gating concurrency via a semaphore and offloading heavy
184    /// work to the blocking pool to avoid stalling the async runtime.
185    async fn build_block_v1(
186        &self,
187        request: TestingBuildBlockRequestV1,
188    ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
189        self.build_block_v1(request).await.map_err(Into::into)
190    }
191}