1use alloy_consensus::BlockHeader;
4use alloy_eips::BlockNumberOrTag;
5use alloy_primitives::U256;
6use alloy_rpc_types_eth::BlockId;
7use alloy_rpc_types_mev::{
8 BundleItem, Inclusion, Privacy, RefundConfig, SendBundleRequest, SimBundleLogs,
9 SimBundleOverrides, SimBundleResponse, Validity,
10};
11use jsonrpsee::core::RpcResult;
12use reth_chainspec::EthChainSpec;
13use reth_evm::{ConfigureEvm, ConfigureEvmEnv};
14use reth_provider::{ChainSpecProvider, HeaderProvider, ProviderTx};
15use reth_revm::database::StateProviderDatabase;
16use reth_rpc_api::MevSimApiServer;
17use reth_rpc_eth_api::{
18 helpers::{Call, EthTransactions, LoadPendingBlock},
19 FromEthApiError, RpcNodeCore,
20};
21use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError};
22use reth_tasks::pool::BlockingTaskGuard;
23use reth_transaction_pool::{PoolConsensusTx, PoolPooledTx, PoolTransaction, TransactionPool};
24use revm::{
25 db::CacheDB,
26 primitives::{Address, EnvWithHandlerCfg, ResultAndState, SpecId, TxEnv},
27 DatabaseCommit, DatabaseRef,
28};
29use std::{sync::Arc, time::Duration};
30use tracing::info;
31
32const MAX_NESTED_BUNDLE_DEPTH: usize = 5;
34
35const MAX_BUNDLE_BODY_SIZE: usize = 50;
37
38const DEFAULT_SIM_TIMEOUT: Duration = Duration::from_secs(5);
40
41const MAX_SIM_TIMEOUT: Duration = Duration::from_secs(30);
43
44const SBUNDLE_PAYOUT_MAX_COST: u64 = 30_000;
46
47#[derive(Clone, Debug)]
49pub struct FlattenedBundleItem<T> {
50 pub tx: T,
52 pub signer: Address,
54 pub can_revert: bool,
56 pub inclusion: Inclusion,
58 pub validity: Option<Validity>,
60 pub privacy: Option<Privacy>,
62 pub refund_percent: Option<u64>,
64 pub refund_configs: Option<Vec<RefundConfig>>,
66}
67
68pub struct EthSimBundle<Eth> {
70 inner: Arc<EthSimBundleInner<Eth>>,
72}
73
74impl<Eth> EthSimBundle<Eth> {
75 pub fn new(eth_api: Eth, blocking_task_guard: BlockingTaskGuard) -> Self {
77 Self { inner: Arc::new(EthSimBundleInner { eth_api, blocking_task_guard }) }
78 }
79
80 pub fn eth_api(&self) -> &Eth {
82 &self.inner.eth_api
83 }
84}
85
86impl<Eth> EthSimBundle<Eth>
87where
88 Eth: EthTransactions + LoadPendingBlock + Call + 'static,
89{
90 fn parse_and_flatten_bundle(
95 &self,
96 request: &SendBundleRequest,
97 ) -> Result<Vec<FlattenedBundleItem<ProviderTx<Eth::Provider>>>, EthApiError> {
98 let mut items = Vec::new();
99
100 let mut stack = Vec::new();
102
103 stack.push((request, 0, 1));
105
106 while let Some((current_bundle, mut idx, depth)) = stack.pop() {
107 if depth > MAX_NESTED_BUNDLE_DEPTH {
109 return Err(EthApiError::InvalidParams(EthSimBundleError::MaxDepth.to_string()));
110 }
111
112 let inclusion = ¤t_bundle.inclusion;
114 let validity = ¤t_bundle.validity;
115 let privacy = ¤t_bundle.privacy;
116
117 let block_number = inclusion.block_number();
119 let max_block_number = inclusion.max_block_number().unwrap_or(block_number);
120
121 if max_block_number < block_number || block_number == 0 {
122 return Err(EthApiError::InvalidParams(
123 EthSimBundleError::InvalidInclusion.to_string(),
124 ));
125 }
126
127 if current_bundle.bundle_body.len() > MAX_BUNDLE_BODY_SIZE {
129 return Err(EthApiError::InvalidParams(
130 EthSimBundleError::BundleTooLarge.to_string(),
131 ));
132 }
133
134 if let Some(validity) = ¤t_bundle.validity {
136 if let Some(refunds) = &validity.refund {
138 let mut total_percent = 0;
139 for refund in refunds {
140 if refund.body_idx as usize >= current_bundle.bundle_body.len() {
141 return Err(EthApiError::InvalidParams(
142 EthSimBundleError::InvalidValidity.to_string(),
143 ));
144 }
145 if 100 - total_percent < refund.percent {
146 return Err(EthApiError::InvalidParams(
147 EthSimBundleError::InvalidValidity.to_string(),
148 ));
149 }
150 total_percent += refund.percent;
151 }
152 }
153
154 if let Some(refund_configs) = &validity.refund_config {
156 let mut total_percent = 0;
157 for refund_config in refund_configs {
158 if 100 - total_percent < refund_config.percent {
159 return Err(EthApiError::InvalidParams(
160 EthSimBundleError::InvalidValidity.to_string(),
161 ));
162 }
163 total_percent += refund_config.percent;
164 }
165 }
166 }
167
168 let body = ¤t_bundle.bundle_body;
169
170 while idx < body.len() {
172 match &body[idx] {
173 BundleItem::Tx { tx, can_revert } => {
174 let recovered_tx = recover_raw_transaction::<PoolPooledTx<Eth::Pool>>(tx)
175 .map_err(EthApiError::from)?;
176 let (tx, signer) = recovered_tx.to_components();
177 let tx: PoolConsensusTx<Eth::Pool> =
178 <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus(tx);
179
180 let refund_percent =
181 validity.as_ref().and_then(|v| v.refund.as_ref()).and_then(|refunds| {
182 refunds.iter().find_map(|refund| {
183 (refund.body_idx as usize == idx).then_some(refund.percent)
184 })
185 });
186 let refund_configs =
187 validity.as_ref().and_then(|v| v.refund_config.clone());
188
189 let flattened_item = FlattenedBundleItem {
191 tx,
192 signer,
193 can_revert: *can_revert,
194 inclusion: inclusion.clone(),
195 validity: validity.clone(),
196 privacy: privacy.clone(),
197 refund_percent,
198 refund_configs,
199 };
200
201 items.push(flattened_item);
203
204 idx += 1;
205 }
206 BundleItem::Bundle { bundle } => {
207 stack.push((current_bundle, idx + 1, depth));
209
210 stack.push((bundle, 0, depth + 1));
212 break;
213 }
214 BundleItem::Hash { hash: _ } => {
215 return Err(EthApiError::InvalidParams(
217 EthSimBundleError::InvalidBundle.to_string(),
218 ));
219 }
220 }
221 }
222 }
223
224 Ok(items)
225 }
226
227 async fn sim_bundle(
228 &self,
229 request: SendBundleRequest,
230 overrides: SimBundleOverrides,
231 logs: bool,
232 ) -> Result<SimBundleResponse, Eth::Error> {
233 let SimBundleOverrides {
234 parent_block,
235 block_number,
236 coinbase,
237 timestamp,
238 gas_limit,
239 base_fee,
240 ..
241 } = overrides;
242
243 let flattened_bundle = self.parse_and_flatten_bundle(&request)?;
246
247 let block_id = parent_block.unwrap_or(BlockId::Number(BlockNumberOrTag::Pending));
248 let (cfg, mut block_env, current_block) = self.eth_api().evm_env_at(block_id).await?;
249
250 let parent_header = RpcNodeCore::provider(&self.inner.eth_api)
251 .header_by_number(block_env.number.saturating_to::<u64>())
252 .map_err(EthApiError::from_eth_err)? .ok_or_else(|| {
254 EthApiError::HeaderNotFound((block_env.number.saturating_to::<u64>()).into())
255 })?;
256
257 if let Some(block_number) = block_number {
259 block_env.number = U256::from(block_number);
260 }
261
262 if let Some(coinbase) = coinbase {
263 block_env.coinbase = coinbase;
264 }
265
266 if let Some(timestamp) = timestamp {
267 block_env.timestamp = U256::from(timestamp);
268 }
269
270 if let Some(gas_limit) = gas_limit {
271 block_env.gas_limit = U256::from(gas_limit);
272 }
273
274 if let Some(base_fee) = base_fee {
275 block_env.basefee = U256::from(base_fee);
276 } else if cfg.handler_cfg.spec_id.is_enabled_in(SpecId::LONDON) {
277 if let Some(base_fee) = parent_header.next_block_base_fee(
278 RpcNodeCore::provider(&self.inner.eth_api)
279 .chain_spec()
280 .base_fee_params_at_block(block_env.number.saturating_to::<u64>()),
281 ) {
282 block_env.basefee = U256::from(base_fee);
283 }
284 }
285
286 let eth_api = self.inner.eth_api.clone();
287
288 let sim_response = self
289 .inner
290 .eth_api
291 .spawn_with_state_at_block(current_block, move |state| {
292 let current_block_number = current_block.as_u64().unwrap();
294 let coinbase = block_env.coinbase;
295 let basefee = block_env.basefee;
296 let env = EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, TxEnv::default());
297 let db = CacheDB::new(StateProviderDatabase::new(state));
298
299 let initial_coinbase_balance = DatabaseRef::basic_ref(&db, coinbase)
300 .map_err(EthApiError::from_eth_err)?
301 .map(|acc| acc.balance)
302 .unwrap_or_default();
303
304 let mut coinbase_balance_before_tx = initial_coinbase_balance;
305 let mut total_gas_used = 0;
306 let mut total_profit = U256::ZERO;
307 let mut refundable_value = U256::ZERO;
308 let mut body_logs: Vec<SimBundleLogs> = Vec::new();
309
310 let mut evm = eth_api.evm_config().evm_with_env(db, env);
311
312 for item in &flattened_bundle {
313 let block_number = item.inclusion.block_number();
315 let max_block_number =
316 item.inclusion.max_block_number().unwrap_or(block_number);
317
318 if current_block_number < block_number ||
319 current_block_number > max_block_number
320 {
321 return Err(EthApiError::InvalidParams(
322 EthSimBundleError::InvalidInclusion.to_string(),
323 )
324 .into());
325 }
326 eth_api
327 .evm_config()
328 .fill_tx_env(evm.tx_mut(), &item.tx, item.signer)
329 .map_err(|_| EthApiError::FailedToDecodeSignedTransaction)?;
330
331 let ResultAndState { result, state } =
332 evm.transact().map_err(EthApiError::from_eth_err)?;
333
334 if !result.is_success() && !item.can_revert {
335 return Err(EthApiError::InvalidParams(
336 EthSimBundleError::BundleTransactionFailed.to_string(),
337 )
338 .into());
339 }
340
341 let gas_used = result.gas_used();
342 total_gas_used += gas_used;
343
344 let coinbase_balance_after_tx =
346 state.get(&coinbase).map(|acc| acc.info.balance).unwrap_or_default();
347
348 let coinbase_diff =
349 coinbase_balance_after_tx.saturating_sub(coinbase_balance_before_tx);
350 total_profit += coinbase_diff;
351
352 if item.refund_percent.is_none() {
354 refundable_value += coinbase_diff;
355 }
356
357 coinbase_balance_before_tx = coinbase_balance_after_tx;
359
360 if logs {
364 let tx_logs = result.logs().to_vec();
365 let sim_bundle_logs =
366 SimBundleLogs { tx_logs: Some(tx_logs), bundle_logs: None };
367 body_logs.push(sim_bundle_logs);
368 }
369
370 evm.context.evm.db.commit(state);
372 }
373
374 for item in &flattened_bundle {
376 if let Some(refund_percent) = item.refund_percent {
377 let refund_configs = item.refund_configs.clone().unwrap_or_else(|| {
379 vec![RefundConfig { address: item.signer, percent: 100 }]
380 });
381
382 let payout_tx_fee = basefee *
384 U256::from(SBUNDLE_PAYOUT_MAX_COST) *
385 U256::from(refund_configs.len() as u64);
386
387 total_gas_used += SBUNDLE_PAYOUT_MAX_COST * refund_configs.len() as u64;
389
390 let payout_value =
392 refundable_value * U256::from(refund_percent) / U256::from(100);
393
394 if payout_tx_fee > payout_value {
395 return Err(EthApiError::InvalidParams(
396 EthSimBundleError::NegativeProfit.to_string(),
397 )
398 .into());
399 }
400
401 total_profit = total_profit.checked_sub(payout_value).ok_or(
403 EthApiError::InvalidParams(
404 EthSimBundleError::NegativeProfit.to_string(),
405 ),
406 )?;
407
408 refundable_value = refundable_value.checked_sub(payout_value).ok_or(
410 EthApiError::InvalidParams(
411 EthSimBundleError::NegativeProfit.to_string(),
412 ),
413 )?;
414 }
415 }
416
417 let mev_gas_price = if total_gas_used != 0 {
419 total_profit / U256::from(total_gas_used)
420 } else {
421 U256::ZERO
422 };
423
424 Ok(SimBundleResponse {
425 success: true,
426 state_block: current_block_number,
427 error: None,
428 logs: Some(body_logs),
429 gas_used: total_gas_used,
430 mev_gas_price,
431 profit: total_profit,
432 refundable_value,
433 exec_error: None,
434 revert: None,
435 })
436 })
437 .await
438 .map_err(|_| {
439 EthApiError::InvalidParams(EthSimBundleError::BundleTimeout.to_string())
440 })?;
441
442 Ok(sim_response)
443 }
444}
445
446#[async_trait::async_trait]
447impl<Eth> MevSimApiServer for EthSimBundle<Eth>
448where
449 Eth: EthTransactions + LoadPendingBlock + Call + 'static,
450{
451 async fn sim_bundle(
452 &self,
453 request: SendBundleRequest,
454 overrides: SimBundleOverrides,
455 ) -> RpcResult<SimBundleResponse> {
456 info!("mev_simBundle called, request: {:?}, overrides: {:?}", request, overrides);
457
458 let override_timeout = overrides.timeout;
459
460 let timeout = override_timeout
461 .map(Duration::from_secs)
462 .filter(|&custom_duration| custom_duration <= MAX_SIM_TIMEOUT)
463 .unwrap_or(DEFAULT_SIM_TIMEOUT);
464
465 let bundle_res =
466 tokio::time::timeout(timeout, Self::sim_bundle(self, request, overrides, true))
467 .await
468 .map_err(|_| {
469 EthApiError::InvalidParams(EthSimBundleError::BundleTimeout.to_string())
470 })?;
471
472 bundle_res.map_err(Into::into)
473 }
474}
475
476#[derive(Debug)]
478struct EthSimBundleInner<Eth> {
479 #[allow(dead_code)]
481 eth_api: Eth,
482 #[allow(dead_code)]
484 blocking_task_guard: BlockingTaskGuard,
485}
486
487impl<Eth> std::fmt::Debug for EthSimBundle<Eth> {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct("EthSimBundle").finish_non_exhaustive()
490 }
491}
492
493impl<Eth> Clone for EthSimBundle<Eth> {
494 fn clone(&self) -> Self {
495 Self { inner: Arc::clone(&self.inner) }
496 }
497}
498
499#[derive(Debug, thiserror::Error)]
501pub enum EthSimBundleError {
502 #[error("max depth reached")]
504 MaxDepth,
505 #[error("unmatched bundle")]
507 UnmatchedBundle,
508 #[error("bundle too large")]
510 BundleTooLarge,
511 #[error("invalid validity")]
513 InvalidValidity,
514 #[error("invalid inclusion")]
516 InvalidInclusion,
517 #[error("invalid bundle")]
519 InvalidBundle,
520 #[error("bundle simulation timed out")]
522 BundleTimeout,
523 #[error("bundle transaction failed")]
525 BundleTransactionFailed,
526 #[error("bundle simulation returned negative profit")]
528 NegativeProfit,
529}