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