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::{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 reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
41use revm::context::Block;
42use revm_primitives::map::DefaultHashBuilder;
43use std::sync::Arc;
44use tracing::debug;
45
46/// Testing API handler.
47#[derive(Debug, Clone)]
48pub struct TestingApi<Eth, Evm> {
49    eth_api: Eth,
50    evm_config: Evm,
51    /// Desired gas limit to move toward while respecting the consensus gas limit bounds.
52    desired_gas_limit: u64,
53    /// If true, skip invalid transactions instead of failing.
54    skip_invalid_transactions: bool,
55    /// If set, override the block gas limit in `testing_buildBlockV1`.
56    gas_limit_override: Option<u64>,
57}
58
59impl<Eth, Evm> TestingApi<Eth, Evm> {
60    /// Create a new testing API handler.
61    pub const fn new(eth_api: Eth, evm_config: Evm, desired_gas_limit: u64) -> Self {
62        Self {
63            eth_api,
64            evm_config,
65            desired_gas_limit,
66            skip_invalid_transactions: false,
67            gas_limit_override: None,
68        }
69    }
70
71    /// Enable skipping invalid transactions instead of failing.
72    /// When a transaction fails, all subsequent transactions from the same sender are also
73    /// skipped.
74    pub const fn with_skip_invalid_transactions(mut self) -> Self {
75        self.skip_invalid_transactions = true;
76        self
77    }
78
79    /// Override the gas limit used by `testing_buildBlockV1`.
80    pub const fn with_gas_limit_override(mut self, gas_limit: u64) -> Self {
81        self.gas_limit_override = Some(gas_limit);
82        self
83    }
84}
85
86impl<Eth, Evm> TestingApi<Eth, Evm>
87where
88    Eth: Call<
89        Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
90        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
91    >,
92    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
93        + 'static,
94{
95    async fn build_block_v1(
96        &self,
97        request: TestingBuildBlockRequestV1,
98    ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
99        let evm_config = self.evm_config.clone();
100        let skip_invalid_transactions = self.skip_invalid_transactions;
101        let desired_gas_limit = self.desired_gas_limit;
102        let gas_limit_override = self.gas_limit_override;
103        self.eth_api
104            .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
105                let state = state.database.0;
106                let mut db = State::builder()
107                    .with_bundle_update()
108                    .with_database(StateProviderDatabase::new(&state))
109                    .build();
110                let parent = eth_api
111                    .provider()
112                    .sealed_header_by_hash(request.parent_block_hash)?
113                    .ok_or_else(|| {
114                    EthApiError::HeaderNotFound(request.parent_block_hash.into())
115                })?;
116
117                let chain_spec = eth_api.provider().chain_spec();
118                let is_osaka =
119                    chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
120
121                let withdrawals = request.payload_attributes.withdrawals.clone();
122                let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);
123
124                let env_attrs = NextBlockEnvAttributes {
125                    timestamp: request.payload_attributes.timestamp,
126                    suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
127                    prev_randao: request.payload_attributes.prev_randao,
128                    gas_limit: gas_limit_override.unwrap_or_else(|| {
129                        calculate_block_gas_limit(parent.gas_limit(), desired_gas_limit)
130                    }),
131                    parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
132                    withdrawals: withdrawals.map(Into::into),
133                    extra_data: request.extra_data.unwrap_or_default(),
134                    slot_number: request.payload_attributes.slot_number,
135                };
136
137                let mut builder = evm_config
138                    .builder_for_next_block(&mut db, &parent, env_attrs)
139                    .map_err(RethError::other)
140                    .map_err(Eth::Error::from_eth_err)?;
141                builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
142
143                let mut total_fees = U256::ZERO;
144                let base_fee = builder.evm_mut().block().basefee();
145
146                let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
147                let mut block_transactions_rlp_length = 0usize;
148
149                // If no transactions are provided in the request, use transactions from the pool.
150                let use_pool_transactions = request.transactions.is_empty();
151                let recovered_txs = if use_pool_transactions {
152                    let mut best_txs = eth_api.pool().best_transactions_with_attributes(
153                        BestTransactionsAttributes::new(
154                            base_fee,
155                            builder
156                                .evm_mut()
157                                .block()
158                                .blob_gasprice()
159                                .map(|gasprice| gasprice as u64),
160                        ),
161                    );
162                    best_txs.no_updates();
163                    best_txs.map(|tx| tx.to_consensus()).collect()
164                } else {
165                    // Decode and recover all transactions in parallel
166                    try_recover_signers(&request.transactions, |tx| {
167                        TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
168                            .map_err(RecoveryError::from_source)
169                    })
170                    .or(Err(EthApiError::InvalidTransactionSignature))?
171                };
172                let allow_skip_invalid_transactions =
173                    skip_invalid_transactions || use_pool_transactions;
174
175                for (idx, tx) in recovered_txs.into_iter().enumerate() {
176                    let signer = tx.signer();
177                    if allow_skip_invalid_transactions && invalid_senders.contains(&signer) {
178                        continue;
179                    }
180
181                    // EIP-7934: Check estimated block size before adding transaction
182                    let tx_rlp_len = tx.tx().length();
183                    if is_osaka {
184                        // 1KB overhead for block header
185                        let estimated_block_size = block_transactions_rlp_length +
186                            tx_rlp_len +
187                            withdrawals_rlp_length +
188                            1024;
189                        if estimated_block_size > MAX_RLP_BLOCK_SIZE {
190                            if allow_skip_invalid_transactions {
191                                debug!(
192                                    target: "rpc::testing",
193                                    tx_idx = idx,
194                                    ?signer,
195                                    estimated_block_size,
196                                    max_size = MAX_RLP_BLOCK_SIZE,
197                                    "Skipping transaction: would exceed block size limit"
198                                );
199                                invalid_senders.insert(signer);
200                                continue;
201                            }
202                            return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
203                                format!(
204                                    "transaction at index {} would exceed max block size: {} > {}",
205                                    idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
206                                ),
207                            )));
208                        }
209                    }
210
211                    let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
212                    let gas_used = match builder.execute_transaction(tx) {
213                        Ok(gas_used) => gas_used.tx_gas_used(),
214                        Err(err) => {
215                            if allow_skip_invalid_transactions {
216                                debug!(
217                                    target: "rpc::testing",
218                                    tx_idx = idx,
219                                    ?signer,
220                                    error = ?err,
221                                    "Skipping invalid transaction"
222                                );
223                                invalid_senders.insert(signer);
224                                continue;
225                            }
226                            debug!(
227                                target: "rpc::testing",
228                                tx_idx = idx,
229                                ?signer,
230                                error = ?err,
231                                "Transaction execution failed"
232                            );
233                            return Err(Eth::Error::from_eth_err(err));
234                        }
235                    };
236
237                    block_transactions_rlp_length += tx_rlp_len;
238                    total_fees += U256::from(tip) * U256::from(gas_used);
239                }
240                let outcome = builder.finish(&state, None).map_err(Eth::Error::from_eth_err)?;
241
242                let has_requests = outcome.block.requests_hash().is_some();
243                let requests = has_requests.then_some(outcome.execution_result.requests);
244
245                EthBuiltPayload::new(Arc::new(outcome.block), total_fees, requests, None)
246                    .try_into_v5()
247                    .map_err(RethError::other)
248                    .map_err(Eth::Error::from_eth_err)
249            })
250            .await
251    }
252}
253
254#[async_trait]
255impl<Eth, Evm> TestingApiServer for TestingApi<Eth, Evm>
256where
257    Eth: Call<
258        Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
259        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
260    >,
261    Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
262        + 'static,
263{
264    /// Handles `testing_buildBlockV1` by gating concurrency via a semaphore and offloading heavy
265    /// work to the blocking pool to avoid stalling the async runtime.
266    async fn build_block_v1(
267        &self,
268        request: TestingBuildBlockRequestV1,
269    ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
270        self.build_block_v1(request).await.map_err(Into::into)
271    }
272}