1use 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#[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: u64,
64 engine_handle: ConsensusEngineHandle<Payload>,
65 skip_invalid_transactions: bool,
67 gas_limit_override: Option<u64>,
69}
70
71impl<Eth, Evm, Payload: PayloadTypes> TestingApi<Eth, Evm, Payload> {
72 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 pub const fn with_skip_invalid_transactions(mut self) -> Self {
93 self.skip_invalid_transactions = true;
94 self
95 }
96
97 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 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 best_txs.skip_blobs();
196 best_txs.map(|tx| tx.to_consensus()).collect()
197 } else {
198 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 let tx_rlp_len = tx.inner().length();
216 if is_osaka {
217 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 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 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}