1use alloy_consensus::{Header, Transaction};
18use alloy_eips::eip2718::Decodable2718;
19use alloy_evm::Evm;
20use alloy_primitives::{map::HashSet, Address, U256};
21use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5;
22use async_trait::async_trait;
23use jsonrpsee::core::RpcResult;
24use reth_errors::RethError;
25use reth_ethereum_engine_primitives::EthBuiltPayload;
26use reth_ethereum_primitives::EthPrimitives;
27use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes};
28use reth_primitives_traits::{
29 transaction::{recover::try_recover_signers, signed::RecoveryError},
30 AlloyBlockHeader as BlockTrait, TxTy,
31};
32use reth_revm::{database::StateProviderDatabase, db::State};
33use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1};
34use reth_rpc_eth_api::{helpers::Call, FromEthApiError};
35use reth_rpc_eth_types::EthApiError;
36use reth_storage_api::{BlockReader, HeaderProvider};
37use revm::context::Block;
38use revm_primitives::map::DefaultHashBuilder;
39use std::sync::Arc;
40use tracing::debug;
41
42#[derive(Debug, Clone)]
44pub struct TestingApi<Eth, Evm> {
45 eth_api: Eth,
46 evm_config: Evm,
47 skip_invalid_transactions: bool,
49}
50
51impl<Eth, Evm> TestingApi<Eth, Evm> {
52 pub const fn new(eth_api: Eth, evm_config: Evm) -> Self {
54 Self { eth_api, evm_config, skip_invalid_transactions: false }
55 }
56
57 pub const fn with_skip_invalid_transactions(mut self) -> Self {
61 self.skip_invalid_transactions = true;
62 self
63 }
64}
65
66impl<Eth, Evm> TestingApi<Eth, Evm>
67where
68 Eth: Call<Provider: BlockReader<Header = Header>>,
69 Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
70 + 'static,
71{
72 async fn build_block_v1(
73 &self,
74 request: TestingBuildBlockRequestV1,
75 ) -> Result<ExecutionPayloadEnvelopeV5, Eth::Error> {
76 let evm_config = self.evm_config.clone();
77 let skip_invalid_transactions = self.skip_invalid_transactions;
78 self.eth_api
79 .spawn_with_state_at_block(request.parent_block_hash, move |eth_api, state| {
80 let state = state.database.0;
81 let mut db = State::builder()
82 .with_bundle_update()
83 .with_database(StateProviderDatabase::new(&state))
84 .build();
85 let parent = eth_api
86 .provider()
87 .sealed_header_by_hash(request.parent_block_hash)?
88 .ok_or_else(|| {
89 EthApiError::HeaderNotFound(request.parent_block_hash.into())
90 })?;
91
92 let env_attrs = NextBlockEnvAttributes {
93 timestamp: request.payload_attributes.timestamp,
94 suggested_fee_recipient: request.payload_attributes.suggested_fee_recipient,
95 prev_randao: request.payload_attributes.prev_randao,
96 gas_limit: parent.gas_limit(),
97 parent_beacon_block_root: request.payload_attributes.parent_beacon_block_root,
98 withdrawals: request.payload_attributes.withdrawals.map(Into::into),
99 extra_data: request.extra_data.unwrap_or_default(),
100 };
101
102 let mut builder = evm_config
103 .builder_for_next_block(&mut db, &parent, env_attrs)
104 .map_err(RethError::other)
105 .map_err(Eth::Error::from_eth_err)?;
106 builder.apply_pre_execution_changes().map_err(Eth::Error::from_eth_err)?;
107
108 let mut total_fees = U256::ZERO;
109 let base_fee = builder.evm_mut().block().basefee();
110
111 let mut invalid_senders: HashSet<Address, DefaultHashBuilder> = HashSet::default();
112
113 let recovered_txs = try_recover_signers(&request.transactions, |tx| {
115 TxTy::<Evm::Primitives>::decode_2718_exact(tx.as_ref())
116 .map_err(RecoveryError::from_source)
117 })
118 .or(Err(EthApiError::InvalidTransactionSignature))?;
119
120 for (idx, tx) in recovered_txs.into_iter().enumerate() {
121 let signer = tx.signer();
122 if skip_invalid_transactions && invalid_senders.contains(&signer) {
123 continue;
124 }
125
126 let tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default();
127 let gas_used = match builder.execute_transaction(tx) {
128 Ok(gas_used) => gas_used,
129 Err(err) => {
130 if skip_invalid_transactions {
131 debug!(
132 target: "rpc::testing",
133 tx_idx = idx,
134 ?signer,
135 error = ?err,
136 "Skipping invalid transaction"
137 );
138 invalid_senders.insert(signer);
139 continue;
140 }
141 debug!(
142 target: "rpc::testing",
143 tx_idx = idx,
144 ?signer,
145 error = ?err,
146 "Transaction execution failed"
147 );
148 return Err(Eth::Error::from_eth_err(err));
149 }
150 };
151
152 total_fees += U256::from(tip) * U256::from(gas_used);
153 }
154 let outcome = builder.finish(&state).map_err(Eth::Error::from_eth_err)?;
155
156 let requests = outcome
157 .block
158 .requests_hash()
159 .is_some()
160 .then_some(outcome.execution_result.requests);
161
162 EthBuiltPayload::new(
163 alloy_rpc_types_engine::PayloadId::default(),
164 Arc::new(outcome.block.into_sealed_block()),
165 total_fees,
166 requests,
167 )
168 .try_into_v5()
169 .map_err(RethError::other)
170 .map_err(Eth::Error::from_eth_err)
171 })
172 .await
173 }
174}
175
176#[async_trait]
177impl<Eth, Evm> TestingApiServer for TestingApi<Eth, Evm>
178where
179 Eth: Call<Provider: BlockReader<Header = Header>>,
180 Evm: ConfigureEvm<NextBlockEnvCtx = NextBlockEnvAttributes, Primitives = EthPrimitives>
181 + 'static,
182{
183 async fn build_block_v1(
186 &self,
187 request: TestingBuildBlockRequestV1,
188 ) -> RpcResult<ExecutionPayloadEnvelopeV5> {
189 self.build_block_v1(request).await.map_err(Into::into)
190 }
191}