1use alloy_consensus::{Header, Transaction};
18use alloy_eips::eip2718::Decodable2718;
19use alloy_evm::{Evm, RecoveredTx};
20use alloy_primitives::{map::HashSet, Address, U256};
21use alloy_rlp::Encodable;
22use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5;
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_errors::RethError;
28use reth_ethereum_engine_primitives::EthBuiltPayload;
29use reth_ethereum_primitives::EthPrimitives;
30use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
31use reth_primitives_traits::{
32 transaction::{recover::try_recover_signers, signed::RecoveryError},
33 AlloyBlockHeader as BlockTrait, TxTy,
34};
35use reth_revm::{database::StateProviderDatabase, db::State};
36use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
37use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
38use reth_rpc_eth_types::EthApiError;
39use reth_storage_api::{BlockReader, HeaderProvider};
40use revm::context::Block;
41use revm_primitives::map::DefaultHashBuilder;
42use std::sync::Arc;
43use tracing::debug;
44
45#[derive(Debug, Clone)]
47pub struct TestingApi<Eth, Evm> {
48 eth_api: Eth,
49 evm_config: Evm,
50 skip_invalid_transactions: bool,
52 gas_limit_override: Option<u64>,
54}
55
56impl<Eth, Evm> TestingApi<Eth, Evm> {
57 pub const fn new(eth_api: Eth, evm_config: Evm) -> Self {
59 Self { eth_api, evm_config, skip_invalid_transactions: false, gas_limit_override: None }
60 }
61
62 pub const fn with_skip_invalid_transactions(mut self) -> Self {
66 self.skip_invalid_transactions = true;
67 self
68 }
69
70 pub const fn with_gas_limit_override(mut self, gas_limit: u64) -> Self {
73 self.gas_limit_override = Some(gas_limit);
74 self
75 }
76}
77
78impl<Eth, Evm> TestingApi<Eth, Evm>
79where
80 Eth: Call<
81 Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
82 >,
83 Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
84 + 'static,
85{
86 async fn build_block_v1(
87 &self,
88 request: TestingBuildBlockRequestV1,
89 ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
90 let evm_config = self.evm_config.clone();
91 let skip_invalid_transactions = self.skip_invalid_transactions;
92 let gas_limit_override = self.gas_limit_override;
93 self.eth_api
94 .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
95 let state = state.database.0;
96 let mut db = State::builder()
97 .with_bundle_update()
98 .with_database(StateProviderDatabase::new(&state))
99 .build();
100 let parent = eth_api
101 .provider()
102 .sealed_header_by_hash(request.parent_block_hash)?
103 .ok_or_else(|| {
104 EthApiError::HeaderNotFound(request.parent_block_hash.into())
105 })?;
106
107 let chain_spec = eth_api.provider().chain_spec();
108 let is_osaka =
109 chain_spec.is_osaka_active_at_timestamp(request.payload_attributes.timestamp);
110
111 let withdrawals = request.payload_attributes.withdrawals.clone();
112 let withdrawals_rlp_length = withdrawals.as_ref().map(|w| w.length()).unwrap_or(0);
113
114 let env_attrs = NextBlockEnvAttributes {
115 timestamp: request.payload_attributes.timestamp,
116 suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
117 prev_randao: request.payload_attributes.prev_randao,
118 gas_limit: gas_limit_override.unwrap_or_else(|| parent.gas_limit()),
119 parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
120 withdrawals: withdrawals.map(Into::into),
121 extra_data: request.extra_data.unwrap_or_default(),
122 };
123
124 let mut builder = evm_config
125 .builder_for_next_block(&mut db, &parent, env_attrs)
126 .map_err(RethError::other)
127 .map_err(Eth::Error::from_eth_err)?;
128 builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
129
130 let mut total_fees = U256::ZERO;
131 let base_fee = builder.evm_mut().block().basefee();
132
133 let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
134 let mut block_transactions_rlp_length = 0usize;
135
136 let recovered_txs = try_recover_signers(&request.transactions, |tx| {
138 TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
139 .map_err(RecoveryError::from_source)
140 })
141 .or(Err(EthApiError::InvalidTransactionSignature))?;
142
143 for (idx, tx) in recovered_txs.into_iter().enumerate() {
144 let signer = tx.signer();
145 if skip_invalid_transactions && invalid_senders.contains(&signer) {
146 continue;
147 }
148
149 let tx_rlp_len = tx.tx().length();
151 if is_osaka {
152 let estimated_block_size = block_transactions_rlp_length +
154 tx_rlp_len +
155 withdrawals_rlp_length +
156 1024;
157 if estimated_block_size > MAX_RLP_BLOCK_SIZE {
158 if skip_invalid_transactions {
159 debug!(
160 target: "rpc::testing",
161 tx_idx = idx,
162 ?signer,
163 estimated_block_size,
164 max_size = MAX_RLP_BLOCK_SIZE,
165 "Skipping transaction: would exceed block size limit"
166 );
167 invalid_senders.insert(signer);
168 continue;
169 }
170 return Err(Eth::Error::from_eth_err(EthApiError::InvalidParams(
171 format!(
172 "transaction at index {} would exceed max block size: {} > {}",
173 idx, estimated_block_size, MAX_RLP_BLOCK_SIZE
174 ),
175 )));
176 }
177 }
178
179 let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
180 let gas_used = match builder.execute_transaction(tx) {
181 Ok(gas_used) => gas_used,
182 Err(err) => {
183 if skip_invalid_transactions {
184 debug!(
185 target: "rpc::testing",
186 tx_idx = idx,
187 ?signer,
188 error = ?err,
189 "Skipping invalid transaction"
190 );
191 invalid_senders.insert(signer);
192 continue;
193 }
194 debug!(
195 target: "rpc::testing",
196 tx_idx = idx,
197 ?signer,
198 error = ?err,
199 "Transaction execution failed"
200 );
201 return Err(Eth::Error::from_eth_err(err));
202 }
203 };
204
205 block_transactions_rlp_length += tx_rlp_len;
206 total_fees += U256::from(tip) * U256::from(gas_used);
207 }
208 let outcome = builder.finish(&state).map_err(Eth::Error::from_eth_err)?;
209
210 let has_requests = outcome.block.requests_hash().is_some();
211 let sealed_block = Arc::new(outcome.block.into_sealed_block());
212
213 let requests = has_requests.then_some(outcome.execution_result.requests);
214
215 EthBuiltPayload::new(sealed_block, total_fees, requests)
216 .try_into_v5()
217 .map_err(RethError::other)
218 .map_err(Eth::Error::from_eth_err)
219 })
220 .await
221 }
222}
223
224#[async_trait]
225impl<Eth, Evm> TestingApiServer for TestingApi<Eth, Evm>
226where
227 Eth: Call<
228 Provider: BlockReader<Header = Header> + ChainSpecProvider<ChainSpec: EthereumHardforks>,
229 >,
230 Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
231 + 'static,
232{
233 async fn build_block_v1(
236 &self,
237 request: TestingBuildBlockRequestV1,
238 ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
239 self.build_block_v1(request).await.map_err(Into::into)
240 }
241}