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