1use alloy_consensus::Transaction;
18use alloy_eips::{eip1559::calculate_block_gas_limit, eip2718::Decodable2718};
19use alloy_evm::{Evm, RecoveredTx};
20use alloy_primitives::{
21 map::{DefaultHashBuilder, HashSet},
22 Address, Bytes, B256, U256,
23};
24use alloy_rlp::Encodable;
25use alloy_rpc_types_engine::{
26 BlobsBundleV2, ExecutionData, ExecutionPayloadEnvelopeV5, ExecutionPayloadEnvelopeV6,
27 ExecutionPayloadSidecar, ExecutionPayloadV3, ExecutionPayloadV4, ForkchoiceState,
28 PayloadAttributes, PraguePayloadFields,
29};
30use async_trait::async_trait;
31use jsonrpsee::core::RpcResult;
32use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
33use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
34use reth_engine_primitives::ConsensusEngineHandle;
35use reth_errors::RethError;
36use reth_ethereum_engine_primitives::EthBuiltPayload;
37use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
38use reth_payload_primitives::{BuiltPayload, PayloadTypes};
39use reth_primitives_traits::{
40 transaction::{recover::try_recover_signers, signed::RecoveryError},
41 AlloyBlockHeader as BlockTrait, Block as _, HeaderTy, TxTy,
42};
43use reth_revm::{database::StateProviderDatabase, db::State};
44use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1, TestingBuildBlockResponseV1};
45use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
46use reth_rpc_eth_types::EthApiError;
47use reth_storage_api::{BlockReader, BlockReaderIdExt, HeaderProvider};
48use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
49use revm::context::Block;
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 = state.database.into_inner();
130 let parent = eth_api
131 .provider()
132 .sealed_header_by_hash(request.parent_block_hash)?
133 .ok_or_else(|| {
134 EthApiError::HeaderNotFound(request.parent_block_hash.into())
135 })?;
136
137 let chain_spec = eth_api.provider().chain_spec();
138 let is_amsterdam = chain_spec
139 .is_amsterdam_active_at_timestamp(request.payload_attributes.timestamp);
140 let is_osaka =
141 chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
142 let mut db = State::builder()
143 .with_bundle_update()
144 .with_database(StateProviderDatabase::new(&state))
145 .with_bal_builder_if(is_amsterdam)
146 .build();
147
148 let withdrawals = request.payload_attributes.withdrawals.clone();
149 let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);
150
151 let env_attrs = NextBlockEnvAttributes {
152 timestamp: request.payload_attributes.timestamp,
153 suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
154 prev_randao: request.payload_attributes.prev_randao,
155 gas_limit: gas_limit_override.unwrap_or_else(|| {
156 calculate_block_gas_limit(
157 parent.gas_limit(),
158 request
159 .payload_attributes
160 .target_gas_limit
161 .unwrap_or(desired_gas_limit),
162 )
163 }),
164 parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
165 withdrawals: withdrawals.map(Into::into),
166 extra_data: request.extra_data.unwrap_or_default(),
167 slot_number: request.payload_attributes.slot_number,
168 };
169
170 let mut builder = evm_config
171 .builder_for_next_block(&mut db, &parent, env_attrs)
172 .map_err(RethError::other)
173 .map_err(Eth::Error::from_eth_err)?;
174 builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
175
176 let mut total_fees = U256::ZERO;
177 let base_fee = builder.evm_mut().block().basefee();
178
179 let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
180 let mut block_transactions_rlp_length = 0usize;
181
182 let recovered_txs = if use_pool_transactions {
184 let mut best_txs = eth_api.pool().best_transactions_with_attributes(
185 BestTransactionsAttributes::new(
186 base_fee,
187 builder
188 .evm_mut()
189 .block()
190 .blob_gasprice()
191 .map(|gasprice| gasprice as u64),
192 ),
193 );
194 best_txs.no_updates();
195 best_txs.map(|tx| tx.to_consensus()).collect()
196 } else {
197 try_recover_signers(&request.transactions, |tx| {
199 TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
200 .map_err(RecoveryError::from_source)
201 })
202 .or(Err(EthApiError::InvalidTransactionSignature))?
203 };
204 let allow_skip_invalid_transactions =
205 skip_invalid_transactions || use_pool_transactions;
206
207 for (idx, tx) in recovered_txs.into_iter().enumerate() {
208 let signer = tx.signer();
209 if allow_skip_invalid_transactions && invalid_senders.contains(&signer) {
210 continue;
211 }
212
213 let tx_rlp_len = tx.tx().length();
215 if is_osaka {
216 let estimated_block_size = block_transactions_rlp_length +
218 tx_rlp_len +
219 withdrawals_rlp_length +
220 1024;
221 if estimated_block_size > MAX_RLP_BLOCK_SIZE {
222 if allow_skip_invalid_transactions {
223 debug!(
224 target: "rpc::testing",
225 tx_idx = idx,
226 ?signer,
227 estimated_block_size,
228 max_size = MAX_RLP_BLOCK_SIZE,
229 "Skipping transaction: would exceed block size limit"
230 );
231 invalid_senders.insert(signer);
232 continue;
233 }
234 return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
235 format!(
236 "transaction at index {} would exceed max block size: {} > {}",
237 idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
238 ),
239 )));
240 }
241 }
242
243 let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
244 let gas_used = match builder.execute_transaction(tx) {
245 Ok(gas_used) => gas_used.tx_gas_used(),
246 Err(err) => {
247 if allow_skip_invalid_transactions {
248 debug!(
249 target: "rpc::testing",
250 tx_idx = idx,
251 ?signer,
252 error = ?err,
253 "Skipping invalid transaction"
254 );
255 invalid_senders.insert(signer);
256 continue;
257 }
258 debug!(
259 target: "rpc::testing",
260 tx_idx = idx,
261 ?signer,
262 error = ?err,
263 "Transaction execution failed"
264 );
265 return Err(Eth::Error::from_eth_err(err));
266 }
267 };
268
269 block_transactions_rlp_length += tx_rlp_len;
270 total_fees += U256::from(tip) * U256::from(gas_used);
271 }
272 let outcome = builder.finish(&*state, None).map_err(Eth::Error::from_eth_err)?;
273
274 let has_requests = outcome.block.requests_hash().is_some();
275 let requests = has_requests.then_some(outcome.execution_result.requests);
276 let block_access_list = outcome.block_access_list.map(|bal| bal.split().1);
277
278 Ok(EthBuiltPayload::new(
279 Arc::new(outcome.block),
280 total_fees,
281 requests,
282 block_access_list,
283 ))
284 })
285 .await
286 }
287
288 async fn build_block_v1(
289 &self,
290 request: TestingBuildBlockRequestV1,
291 use_pool_transactions: bool,
292 ) -> Result<TestingBuildBlockResponseV1, Eth::Error> {
293 let payload = self
294 .build_payload_v1(request, self.skip_invalid_transactions, use_pool_transactions)
295 .await?;
296 let fees = payload.fees();
297 let requests = payload.requests().unwrap_or_default();
298 let block_access_list = payload.block_access_list().cloned();
299 let block = Arc::unwrap_or_clone(payload.into_block_arc());
300 let block_hash = block.hash();
301 let block = block.into_block().into_ethereum_block();
302
303 let Some(block_access_list) = block_access_list else {
304 return Ok(TestingBuildBlockResponseV1::V5(ExecutionPayloadEnvelopeV5 {
305 execution_payload: ExecutionPayloadV3::from_block_unchecked(block_hash, &block),
306 block_value: fees,
307 blobs_bundle: BlobsBundleV2::empty(),
308 should_override_builder: false,
309 execution_requests: requests,
310 }))
311 };
312 Ok(TestingBuildBlockResponseV1::V6(ExecutionPayloadEnvelopeV6 {
313 execution_payload: ExecutionPayloadV4::from_block_unchecked_with_bal(
314 block_hash,
315 &block,
316 block_access_list,
317 ),
318 block_value: fees,
319 blobs_bundle: BlobsBundleV2::empty(),
320 should_override_builder: false,
321 execution_requests: requests,
322 }))
323 }
324
325 async fn commit_block_v1(
326 &self,
327 payload_attributes: PayloadAttributes,
328 transactions: Option<Vec<Bytes>>,
329 extra_data: Option<Bytes>,
330 ) -> Result<B256, Eth::Error> {
331 let parent = self
332 .eth_api
333 .provider()
334 .latest_header()
335 .map_err(EthApiError::from)?
336 .ok_or_else(|| EthApiError::HeaderNotFound(alloy_eips::BlockId::latest()))?;
337 let safe_block_hash = self
338 .eth_api
339 .provider()
340 .safe_header()
341 .map_err(EthApiError::from)?
342 .map(|header| header.hash())
343 .unwrap_or_else(|| parent.hash());
344 let finalized_block_hash = self
345 .eth_api
346 .provider()
347 .finalized_header()
348 .map_err(EthApiError::from)?
349 .map(|header| header.hash())
350 .unwrap_or_else(|| parent.hash());
351
352 let use_pool_transactions = transactions.is_none();
353 let payload = self
354 .build_payload_v1(
355 TestingBuildBlockRequestV1 {
356 parent_block_hash: parent.hash(),
357 payload_attributes,
358 transactions: transactions.unwrap_or_default(),
359 extra_data,
360 },
361 false,
362 use_pool_transactions,
363 )
364 .await?;
365
366 let block_hash = payload.block().hash();
367 let requests = payload.requests();
368 let block_access_list = payload.block_access_list().cloned();
369 let block = Arc::unwrap_or_clone(payload.into_block_arc()).into_sealed_block();
370 let execution_data = Payload::block_to_payload(block, block_access_list);
371 let execution_data = match (requests, execution_data.sidecar.cancun()) {
372 (Some(requests), Some(cancun)) => ExecutionData::new(
373 execution_data.payload,
374 ExecutionPayloadSidecar::v4(cancun.clone(), PraguePayloadFields::new(requests)),
375 ),
376 _ => execution_data,
377 };
378 let status = self
379 .engine_handle
380 .new_payload(execution_data)
381 .await
382 .map_err(RethError::other)
383 .map_err(Eth::Error::from_eth_err)?;
384 if !status.is_valid() {
385 return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
386 "new payload returned non-valid status: {:?}",
387 status.status
388 ))));
389 }
390
391 let fcu = self
392 .engine_handle
393 .fork_choice_updated(
394 ForkchoiceState {
395 head_block_hash: block_hash,
396 safe_block_hash,
397 finalized_block_hash,
398 },
399 None,
400 )
401 .await
402 .map_err(RethError::other)
403 .map_err(Eth::Error::from_eth_err)?;
404 if !fcu.is_valid() {
405 return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(format!(
406 "forkchoice update returned non-valid status: {:?}",
407 fcu.payload_status.status
408 ))));
409 }
410
411 Ok(block_hash)
412 }
413}
414
415#[async_trait]
416impl<Eth, Evm, Payload> TestingApiServer for TestingApi<Eth, Evm, Payload>
417where
418 Payload: PayloadTypes<
419 ExecutionData = ExecutionData,
420 BuiltPayload: BuiltPayload<Primitives = Evm::Primitives>,
421 >,
422 Eth: Call<
423 Provider: BlockReader<Header = HeaderTy<Evm::Primitives>>
424 + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>
425 + ChainSpecProvider<ChainSpec: EthereumHardforks>,
426 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>>,
427 >,
428 Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes> + 'static,
429{
430 async fn build_block_v1(
433 &self,
434 parent_block_hash: B256,
435 payload_attributes: PayloadAttributes,
436 transactions: Option<Vec<Bytes>>,
437 extra_data: Option<Bytes>,
438 ) -> RpcResult<TestingBuildBlockResponseV1> {
439 let use_pool_transactions = transactions.is_none();
440 let request = TestingBuildBlockRequestV1 {
441 parent_block_hash,
442 payload_attributes,
443 transactions: transactions.unwrap_or_default(),
444 extra_data,
445 };
446 self.build_block_v1(request, use_pool_transactions).await.map_err(Into::into)
447 }
448
449 async fn commit_block_v1(
452 &self,
453 payload_attributes: PayloadAttributes,
454 transactions: Option<Vec<Bytes>>,
455 extra_data: Option<Bytes>,
456 ) -> RpcResult<B256> {
457 self.commit_block_v1(payload_attributes, transactions, extra_data).await.map_err(Into::into)
458 }
459}