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