1use crate::eth::bundle::{BundleBlockGas, EthBundleError};
4use alloy_consensus::{transaction::TxHashRef, BlockHeader, Transaction as _};
5use alloy_primitives::U256;
6use alloy_rpc_types_eth::{BlockId, Log};
7use alloy_rpc_types_mev::{
8 BundleItem, Inclusion, MevSendBundle, Privacy, RefundConfig, SimBundleLogs, SimBundleOverrides,
9 SimBundleResponse, Validity,
10};
11use evm2::evm::DynDatabase;
12use jsonrpsee::core::RpcResult;
13use reth_errors::RethError;
14use reth_evm::{ConfigureEvm, Evm, EvmEnv};
15use reth_primitives_traits::Recovered;
16use reth_rpc_api::MevSimApiServer;
17use reth_rpc_eth_api::{
18 helpers::{block::LoadBlock, Call, EthTransactions},
19 FromEthApiError, FromEvmError,
20};
21use reth_rpc_eth_types::{
22 cache::db::apply_block_overrides, EthApiError, RpcInvalidTransactionError,
23};
24use reth_storage_api::ProviderTx;
25use reth_tasks::{cancel::is_cancelled, pool::BlockingTaskGuard};
26use reth_transaction_pool::{PoolPooledTx, PoolTransaction, TransactionPool};
27use std::{sync::Arc, time::Duration};
28use tracing::trace;
29
30const MAX_NESTED_BUNDLE_DEPTH: usize = 5;
32
33const MAX_BUNDLE_BODY_SIZE: usize = 50;
35
36pub(crate) const DEFAULT_SIM_TIMEOUT: Duration = Duration::from_secs(5);
38
39pub(crate) const MAX_SIM_TIMEOUT: Duration = Duration::from_secs(30);
41
42const SBUNDLE_PAYOUT_MAX_COST: u64 = 30_000;
44
45#[derive(Clone, Debug)]
47pub struct FlattenedBundleItem<T> {
48 pub tx: Recovered<T>,
50 pub can_revert: bool,
52 pub inclusion: Inclusion,
54 pub validity: Option<Validity>,
56 pub privacy: Option<Privacy>,
58 pub refund_percent: Option<u64>,
60 pub refund_configs: Option<Vec<RefundConfig>>,
62}
63
64pub struct EthSimBundle<Eth> {
66 inner: Arc<EthSimBundleInner<Eth>>,
68}
69
70impl<Eth> EthSimBundle<Eth> {
71 pub fn new(eth_api: Eth, blocking_task_guard: BlockingTaskGuard) -> Self {
73 Self { inner: Arc::new(EthSimBundleInner { eth_api, blocking_task_guard }) }
74 }
75
76 pub fn eth_api(&self) -> &Eth {
78 &self.inner.eth_api
79 }
80
81 fn build_bundle_logs(
83 bundle: &MevSendBundle,
84 flat_logs: &[Vec<Log>],
85 ) -> Result<Vec<SimBundleLogs>, EthApiError> {
86 struct BundleFrame<'a> {
87 bundle: &'a MevSendBundle,
88 next_idx: usize,
89 logs: Vec<SimBundleLogs>,
90 }
91
92 let mut stack = vec![BundleFrame { bundle, next_idx: 0, logs: Vec::new() }];
93 let mut flat_log_idx = 0;
94 let mut root_logs = None;
95
96 while let Some(mut frame) = stack.pop() {
97 if frame.next_idx == frame.bundle.bundle_body.len() {
98 if let Some(parent) = stack.last_mut() {
99 parent
100 .logs
101 .push(SimBundleLogs { tx_logs: None, bundle_logs: Some(frame.logs) });
102 } else {
103 root_logs = Some(frame.logs);
104 }
105
106 continue;
107 }
108
109 match &frame.bundle.bundle_body[frame.next_idx] {
110 BundleItem::Tx { .. } => {
111 let tx_logs = flat_logs.get(flat_log_idx).cloned().ok_or_else(|| {
112 EthApiError::InvalidParams(EthSimBundleError::UnmatchedBundle.to_string())
113 })?;
114
115 frame.logs.push(SimBundleLogs { tx_logs: Some(tx_logs), bundle_logs: None });
116 frame.next_idx += 1;
117 flat_log_idx += 1;
118 stack.push(frame);
119 }
120 BundleItem::Bundle { bundle } => {
121 frame.next_idx += 1;
122 stack.push(frame);
123 stack.push(BundleFrame { bundle, next_idx: 0, logs: Vec::new() });
124 }
125 BundleItem::Hash { .. } => {
126 return Err(EthApiError::InvalidParams(
127 EthSimBundleError::InvalidBundle.to_string(),
128 ));
129 }
130 }
131 }
132
133 if flat_log_idx != flat_logs.len() {
134 return Err(EthApiError::InvalidParams(EthSimBundleError::UnmatchedBundle.to_string()));
135 }
136
137 root_logs.ok_or_else(|| {
138 EthApiError::InvalidParams(EthSimBundleError::UnmatchedBundle.to_string())
139 })
140 }
141}
142
143impl<Eth> EthSimBundle<Eth>
144where
145 Eth: EthTransactions + LoadBlock + Call,
146{
147 fn parse_and_flatten_bundle(
152 &self,
153 request: &MevSendBundle,
154 ) -> Result<Vec<FlattenedBundleItem<ProviderTx<Eth::Provider>>>, EthApiError> {
155 let mut items = Vec::new();
156
157 let mut stack = Vec::new();
159
160 stack.push((request, 0, 1));
162
163 while let Some((current_bundle, mut idx, depth)) = stack.pop() {
164 if depth > MAX_NESTED_BUNDLE_DEPTH {
166 return Err(EthApiError::InvalidParams(EthSimBundleError::MaxDepth.to_string()));
167 }
168
169 let inclusion = ¤t_bundle.inclusion;
171 let validity = ¤t_bundle.validity;
172 let privacy = ¤t_bundle.privacy;
173
174 let block_number = inclusion.block_number();
176 let max_block_number = inclusion.max_block_number().unwrap_or(block_number);
177
178 if max_block_number < block_number || block_number == 0 {
179 return Err(EthApiError::InvalidParams(
180 EthSimBundleError::InvalidInclusion.to_string(),
181 ));
182 }
183
184 if current_bundle.bundle_body.len() > MAX_BUNDLE_BODY_SIZE {
186 return Err(EthApiError::InvalidParams(
187 EthSimBundleError::BundleTooLarge.to_string(),
188 ));
189 }
190
191 if let Some(validity) = ¤t_bundle.validity {
193 if let Some(refunds) = &validity.refund {
195 let mut total_percent = 0;
196 for refund in refunds {
197 if refund.body_idx as usize >= current_bundle.bundle_body.len() {
198 return Err(EthApiError::InvalidParams(
199 EthSimBundleError::InvalidValidity.to_string(),
200 ));
201 }
202 if 100 - total_percent < refund.percent {
203 return Err(EthApiError::InvalidParams(
204 EthSimBundleError::InvalidValidity.to_string(),
205 ));
206 }
207 total_percent += refund.percent;
208 }
209 }
210
211 if let Some(refund_configs) = &validity.refund_config {
213 let mut total_percent = 0;
214 for refund_config in refund_configs {
215 if 100 - total_percent < refund_config.percent {
216 return Err(EthApiError::InvalidParams(
217 EthSimBundleError::InvalidValidity.to_string(),
218 ));
219 }
220 total_percent += refund_config.percent;
221 }
222 }
223 }
224
225 let body = ¤t_bundle.bundle_body;
226
227 while idx < body.len() {
229 match &body[idx] {
230 BundleItem::Tx { tx, can_revert } => {
231 let recovered_tx = self
232 .eth_api()
233 .recover_raw_transaction::<PoolPooledTx<Eth::Pool>>(tx)?;
234 let tx = recovered_tx.map(
235 <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
236 );
237
238 let refund_percent =
239 validity.as_ref().and_then(|v| v.refund.as_ref()).and_then(|refunds| {
240 refunds.iter().find_map(|refund| {
241 (refund.body_idx as usize == idx).then_some(refund.percent)
242 })
243 });
244 let refund_configs =
245 validity.as_ref().and_then(|v| v.refund_config.clone());
246
247 let flattened_item = FlattenedBundleItem {
249 tx,
250 can_revert: *can_revert,
251 inclusion: inclusion.clone(),
252 validity: validity.clone(),
253 privacy: privacy.clone(),
254 refund_percent,
255 refund_configs,
256 };
257
258 items.push(flattened_item);
259 idx += 1;
260 }
261 BundleItem::Bundle { bundle } => {
262 stack.push((current_bundle, idx + 1, depth));
264
265 stack.push((bundle, 0, depth + 1));
267 break;
268 }
269 BundleItem::Hash { hash: _ } => {
270 return Err(EthApiError::InvalidParams(
272 EthSimBundleError::InvalidBundle.to_string(),
273 ));
274 }
275 }
276 }
277 }
278
279 Ok(items)
280 }
281
282 async fn sim_bundle_inner(
283 &self,
284 request: MevSendBundle,
285 overrides: SimBundleOverrides,
286 logs: bool,
287 ) -> Result<SimBundleResponse, Eth::Error> {
288 let SimBundleOverrides { parent_block, block_overrides, .. } = overrides;
289
290 let call_gas_limit = self.eth_api().call_gas_limit();
292 if let Some(gas_limit) = block_overrides.gas_limit &&
293 call_gas_limit != 0 &&
294 gas_limit > call_gas_limit
295 {
296 return Err(
297 EthApiError::InvalidTransaction(RpcInvalidTransactionError::GasTooHigh).into()
298 )
299 }
300
301 let flattened_bundle = self.parse_and_flatten_bundle(&request)?;
304
305 let block_id = parent_block.unwrap_or(BlockId::latest());
306 let (parent, _, parent_block_id) =
307 self.eth_api().evm_env_and_recovered_block_at(block_id).await?;
308
309 let parent_header = parent.clone_sealed_header();
313 let mut evm_env = {
314 let attributes = self
315 .eth_api()
316 .pending_env_builder()
317 .pending_env_attributes(&parent_header, Some(&block_overrides))?;
318 self.eth_api()
319 .evm_config()
320 .next_evm_env(&parent_header, &attributes)
321 .map_err(RethError::other)
322 .map_err(EthApiError::from_eth_err)?
323 };
324
325 let permit = self
326 .eth_api()
327 .acquire_owned_blocking_io()
328 .await
329 .map_err(|_| EthApiError::InternalEthError)?;
330
331 let eth_api = self.inner.eth_api.clone();
332
333 let sim_response = self
334 .inner
335 .eth_api
336 .spawn_with_state_at_block(parent_block_id, move |_, mut db| {
337 let _permit = permit;
338 apply_block_overrides(block_overrides, &mut db, &mut evm_env);
340
341 let state_block = parent.number();
342 let simulated_block_number = evm_env.block_env().number.saturating_to::<u64>();
343 let simulated_block_timestamp =
344 evm_env.block_env().timestamp.saturating_to::<u64>();
345 let coinbase = evm_env.block_env().beneficiary;
346 let basefee = evm_env.block_base_fee();
347
348 let initial_coinbase_balance = db
349 .get_account(&coinbase)
350 .map_err(Eth::Error::from_eth_err)?
351 .map(|acc| acc.balance)
352 .unwrap_or_default();
353
354 let mut block_gas_limit = evm_env.block_env().gas_limit.saturating_to::<u64>();
355 if call_gas_limit != 0 {
356 block_gas_limit = block_gas_limit.min(call_gas_limit);
357 }
358 let mut block_gas = BundleBlockGas::new(block_gas_limit, evm_env.version());
359 let mut evm = eth_api.evm_config().evm_with_env(&mut db, evm_env);
360 let mut coinbase_balance_before_tx = initial_coinbase_balance;
361 let mut total_profit = U256::ZERO;
362 let mut refundable_value = U256::ZERO;
363 let mut flat_logs: Vec<Vec<Log>> = Vec::new();
364
365 let mut log_index = 0;
366
367 for (tx_index, item) in flattened_bundle.iter().enumerate() {
368 if is_cancelled() {
369 return Err(EthApiError::InternalEthError.into())
370 }
371
372 let block_number = item.inclusion.block_number();
374 let max_block_number =
375 item.inclusion.max_block_number().unwrap_or(block_number);
376
377 if simulated_block_number < block_number ||
378 simulated_block_number > max_block_number
379 {
380 return Err(EthApiError::InvalidParams(
381 EthSimBundleError::InvalidInclusion.to_string(),
382 )
383 .into());
384 }
385
386 if !block_gas.admits(item.tx.gas_limit()) {
387 return Err(EthApiError::InvalidParams(
388 EthBundleError::BundleGasLimitExceeded(block_gas_limit).to_string(),
389 )
390 .into())
391 }
392
393 let tx_env = eth_api.evm_config().tx_env(item.tx.clone());
394 let result = evm.transact_commit(&tx_env).map_err(Eth::Error::from_evm_err)?;
395
396 if !result.status && !item.can_revert {
397 return Err(EthApiError::InvalidParams(
398 EthSimBundleError::BundleTransactionFailed.to_string(),
399 )
400 .into());
401 }
402
403 block_gas.record(&result);
404
405 let coinbase_balance_after_tx = evm
406 .account_info(&coinbase)
407 .map_err(Eth::Error::from_evm_err)?
408 .map(|acc| acc.balance)
409 .unwrap_or_default();
410
411 let coinbase_diff =
412 coinbase_balance_after_tx.saturating_sub(coinbase_balance_before_tx);
413 total_profit += coinbase_diff;
414
415 if item.refund_percent.is_none() {
417 refundable_value += coinbase_diff;
418 }
419
420 coinbase_balance_before_tx = coinbase_balance_after_tx;
422
423 if logs {
426 let tx_logs: Vec<Log> = result
427 .logs
428 .into_iter()
429 .map(|inner| {
430 let full_log = Log {
431 inner,
432 block_hash: None,
434 block_number: Some(simulated_block_number),
435 block_timestamp: Some(simulated_block_timestamp),
436 transaction_hash: Some(*item.tx.tx_hash()),
437 transaction_index: Some(tx_index as u64),
438 log_index: Some(log_index),
439 removed: false,
440 };
441 log_index += 1;
442 full_log
443 })
444 .collect();
445 flat_logs.push(tx_logs);
446 }
447 }
448
449 let body_logs =
450 if logs { Self::build_bundle_logs(&request, &flat_logs)? } else { vec![] };
451
452 let mut total_gas_used = block_gas.gas_used();
453
454 for item in &flattened_bundle {
456 if let Some(refund_percent) = item.refund_percent {
457 let refund_configs = item.refund_configs.clone().unwrap_or_else(|| {
458 vec![RefundConfig { address: item.tx.signer(), percent: 100 }]
459 });
460
461 let payout_tx_fee = U256::from(basefee) *
463 U256::from(SBUNDLE_PAYOUT_MAX_COST) *
464 U256::from(refund_configs.len() as u64);
465
466 total_gas_used += SBUNDLE_PAYOUT_MAX_COST * refund_configs.len() as u64;
468
469 let payout_value =
471 refundable_value * U256::from(refund_percent) / U256::from(100);
472
473 if payout_tx_fee > payout_value {
474 return Err(EthApiError::InvalidParams(
475 EthSimBundleError::NegativeProfit.to_string(),
476 )
477 .into());
478 }
479
480 total_profit = total_profit.checked_sub(payout_value).ok_or(
482 EthApiError::InvalidParams(
483 EthSimBundleError::NegativeProfit.to_string(),
484 ),
485 )?;
486 }
487 }
488
489 let mev_gas_price = if total_gas_used != 0 {
491 total_profit / U256::from(total_gas_used)
492 } else {
493 U256::ZERO
494 };
495
496 Ok(SimBundleResponse {
497 success: true,
498 state_block,
499 error: None,
500 logs: Some(body_logs),
501 gas_used: total_gas_used,
502 mev_gas_price,
503 profit: total_profit,
504 refundable_value,
505 exec_error: None,
506 revert: None,
507 })
508 })
509 .await?;
510
511 Ok(sim_response)
512 }
513}
514
515impl<Eth> MevSimApiServer for EthSimBundle<Eth>
516where
517 Eth: EthTransactions + LoadBlock + Call,
518{
519 async fn sim_bundle(
520 &self,
521 request: MevSendBundle,
522 overrides: SimBundleOverrides,
523 ) -> RpcResult<SimBundleResponse> {
524 trace!("mev_simBundle called, request: {:?}, overrides: {:?}", request, overrides);
525
526 let override_timeout = overrides.timeout;
527
528 let timeout = override_timeout
529 .map(Duration::from_secs)
530 .map(|d| d.min(MAX_SIM_TIMEOUT))
531 .unwrap_or(DEFAULT_SIM_TIMEOUT);
532
533 let bundle_res =
534 tokio::time::timeout(timeout, Self::sim_bundle_inner(self, request, overrides, true))
535 .await
536 .map_err(|_| {
537 EthApiError::InvalidParams(EthSimBundleError::BundleTimeout.to_string())
538 })?;
539
540 bundle_res.map_err(Into::into)
541 }
542}
543
544#[derive(Debug)]
546struct EthSimBundleInner<Eth> {
547 eth_api: Eth,
549 #[expect(dead_code)]
551 blocking_task_guard: BlockingTaskGuard,
552}
553
554impl<Eth> std::fmt::Debug for EthSimBundle<Eth> {
555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556 f.debug_struct("EthSimBundle").finish_non_exhaustive()
557 }
558}
559
560impl<Eth> Clone for EthSimBundle<Eth> {
561 fn clone(&self) -> Self {
562 Self { inner: Arc::clone(&self.inner) }
563 }
564}
565
566#[derive(Debug, thiserror::Error)]
568pub enum EthSimBundleError {
569 #[error("max depth reached")]
571 MaxDepth,
572 #[error("unmatched bundle")]
574 UnmatchedBundle,
575 #[error("bundle too large")]
577 BundleTooLarge,
578 #[error("invalid validity")]
580 InvalidValidity,
581 #[error("invalid inclusion")]
583 InvalidInclusion,
584 #[error("invalid bundle")]
586 InvalidBundle,
587 #[error("bundle simulation timed out")]
589 BundleTimeout,
590 #[error("bundle transaction failed")]
592 BundleTransactionFailed,
593 #[error("bundle simulation returned negative profit")]
595 NegativeProfit,
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::EthApiBuilder;
602 use alloy_consensus::{transaction::SignerRecoverable, TxEip1559};
603 use alloy_eips::{eip1559::INITIAL_BASE_FEE, eip2718::Encodable2718};
604 use alloy_genesis::{Genesis, GenesisAccount};
605 use alloy_primitives::{Address, Bytes, TxKind, B256};
606 use alloy_rpc_types_eth::BlockOverrides;
607 use alloy_rpc_types_mev::{Inclusion, ProtocolVersion};
608 use jsonrpsee::types::ErrorObjectOwned;
609 use reth_chainspec::{ChainSpecBuilder, ChainSpecProvider};
610 use reth_db_common::init::init_genesis;
611 use reth_evm_ethereum::EthEvmConfig;
612 use reth_network_api::noop::NoopNetwork;
613 use reth_provider::{
614 providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
615 };
616 use reth_testing_utils::{
617 generators,
618 generators::{generate_key, sign_tx_with_key_pair},
619 };
620 use reth_transaction_pool::test_utils::testing_pool;
621
622 #[tokio::test(flavor = "multi_thread")]
623 async fn sim_bundle_uses_next_block_env_and_overrides() {
624 let contract = Address::repeat_byte(0xaa);
625 let coinbase = Address::repeat_byte(0x11);
626 let overridden_coinbase = Address::repeat_byte(0x22);
627 let mut rng = generators::rng();
628 let tx = sign_tx_with_key_pair(
629 generate_key(&mut rng),
630 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
631 chain_id: 1,
632 nonce: 0,
633 gas_limit: 100_000,
634 max_fee_per_gas: 2_000_000_000,
635 max_priority_fee_per_gas: 1_000_000_000,
636 to: TxKind::Call(contract),
637 ..Default::default()
638 }),
639 );
640 let sender = tx.recover_signer().unwrap();
641
642 let code = Bytes::from_static(&[0x41, 0x48, 0x42, 0x43, 0x5f, 0x5f, 0xa4, 0x00]);
644 let genesis = Genesis::default()
645 .with_gas_limit(30_000_000)
646 .with_timestamp(100)
647 .with_base_fee(Some(1_000_000_000))
648 .with_coinbase(coinbase)
649 .extend_accounts([
650 (sender, GenesisAccount::default().with_balance(U256::from(10u128.pow(18)))),
651 (contract, GenesisAccount::default().with_code(Some(code))),
652 (
653 overridden_coinbase,
654 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
655 ),
656 ]);
657 let chain_spec =
658 Arc::new(ChainSpecBuilder::mainnet().cancun_activated().genesis(genesis).build());
659 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
660 init_genesis(&factory).unwrap();
661 let provider = BlockchainProvider::new(factory).unwrap();
662
663 let eth_api = EthApiBuilder::new(
664 provider.clone(),
665 testing_pool(),
666 NoopNetwork::default(),
667 EthEvmConfig::new(provider.chain_spec()),
668 )
669 .build();
670 let api = EthSimBundle::new(eth_api, BlockingTaskGuard::new(4));
671
672 let bundle = |block| MevSendBundle {
673 protocol_version: ProtocolVersion::V0_1,
674 inclusion: Inclusion::at_block(block),
675 bundle_body: vec![BundleItem::Tx { tx: tx.encoded_2718().into(), can_revert: false }],
676 validity: None,
677 privacy: None,
678 };
679
680 for (overrides, number, timestamp, base_fee, beneficiary) in [
681 (SimBundleOverrides::default(), 1, 112, 875_000_000, coinbase),
682 (
683 SimBundleOverrides {
684 parent_block: Some(BlockId::number(0)),
685 block_overrides: BlockOverrides {
686 number: Some(U256::from(2)),
687 time: Some(200),
688 base_fee: Some(U256::from(1_500_000_000)),
689 coinbase: Some(overridden_coinbase),
690 ..Default::default()
691 },
692 ..Default::default()
693 },
694 2,
695 200,
696 1_500_000_000,
697 overridden_coinbase,
698 ),
699 ] {
700 let response = api.sim_bundle(bundle(number), overrides.clone()).await.unwrap();
701 assert!(response.success);
702 assert_eq!(response.state_block, 0);
703 let tip = (2_000_000_000u64 - base_fee).min(1_000_000_000);
704 assert_eq!(response.profit, U256::from(response.gas_used) * U256::from(tip));
705 assert_eq!(response.refundable_value, response.profit);
706 assert_eq!(response.mev_gas_price, U256::from(tip));
707
708 let logs = response.logs.unwrap();
709 assert_eq!(logs.len(), 1);
710 let tx_logs = logs[0].tx_logs.as_ref().unwrap();
711 assert_eq!(tx_logs.len(), 1);
712 let log = &tx_logs[0];
713 assert_eq!(
714 log.inner.topics(),
715 &[
716 B256::from(U256::from(number)),
717 B256::from(U256::from(timestamp)),
718 B256::from(U256::from(base_fee)),
719 beneficiary.into_word(),
720 ]
721 );
722 assert_eq!(log.block_hash, None);
723 assert_eq!(log.block_number, Some(number));
724 assert_eq!(log.block_timestamp, Some(timestamp));
725 assert_eq!(log.transaction_hash, Some(*tx.tx_hash()));
726 assert_eq!(log.transaction_index, Some(0));
727 assert_eq!(log.log_index, Some(0));
728
729 let err = api.sim_bundle(bundle(number + 1), overrides.clone()).await.unwrap_err();
731 assert!(err.message().contains("invalid inclusion"), "{err}");
732 if number > 1 {
733 let err = api.sim_bundle(bundle(number - 1), overrides).await.unwrap_err();
734 assert!(err.message().contains("invalid inclusion"), "{err}");
735 }
736 }
737 }
738
739 fn create_test_bundle(tx_bytes: Vec<Bytes>) -> MevSendBundle {
740 let body: Vec<BundleItem> =
741 tx_bytes.into_iter().map(|tx| BundleItem::Tx { tx, can_revert: false }).collect();
742 MevSendBundle {
743 bundle_body: body,
744 inclusion: Inclusion { block: 1, max_block: None },
745 validity: None,
746 privacy: None,
747 protocol_version: ProtocolVersion::V0_1,
748 }
749 }
750
751 fn create_nested_bundle(outer_tx: Bytes, inner_txs: Vec<Bytes>) -> MevSendBundle {
752 let inner_bundle = create_test_bundle(inner_txs);
753 MevSendBundle {
754 bundle_body: vec![
755 BundleItem::Tx { tx: outer_tx, can_revert: false },
756 BundleItem::Bundle { bundle: inner_bundle },
757 ],
758 inclusion: Inclusion { block: 1, max_block: None },
759 validity: None,
760 privacy: None,
761 protocol_version: ProtocolVersion::V0_1,
762 }
763 }
764
765 fn create_bundle_with_body(bundle_body: Vec<BundleItem>) -> MevSendBundle {
766 MevSendBundle {
767 bundle_body,
768 inclusion: Inclusion { block: 1, max_block: None },
769 validity: None,
770 privacy: None,
771 protocol_version: ProtocolVersion::V0_1,
772 }
773 }
774
775 fn create_bundle_logs(log_counts: &[usize]) -> Vec<Vec<Log>> {
776 log_counts.iter().map(|count| vec![Log::default(); *count]).collect()
777 }
778
779 fn assert_unmatched_bundle(result: Result<Vec<SimBundleLogs>, EthApiError>) {
780 assert!(matches!(
781 result,
782 Err(EthApiError::InvalidParams(ref message))
783 if message == &EthSimBundleError::UnmatchedBundle.to_string()
784 ));
785 }
786
787 #[test]
788 fn test_build_bundle_logs_single_tx() {
789 let bundle = create_test_bundle(vec![Bytes::from(vec![0x01, 0x02, 0x03])]);
790 let result =
791 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1])).unwrap();
792
793 assert_eq!(result.len(), 1);
794 assert!(result[0].tx_logs.is_some());
795 assert!(result[0].bundle_logs.is_none());
796 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
797 }
798
799 #[test]
800 fn test_build_bundle_logs_empty_bundle() {
801 let bundle = create_test_bundle(vec![]);
802 let result = EthSimBundle::<()>::build_bundle_logs(&bundle, &[]).unwrap();
803
804 assert!(result.is_empty());
805 }
806
807 #[test]
808 fn test_build_bundle_logs_nested_bundle() {
809 let outer_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
810 let inner_tx1 = Bytes::from(vec![0x04, 0x05, 0x06]);
811 let inner_tx2 = Bytes::from(vec![0x07, 0x08, 0x09]);
812 let bundle = create_nested_bundle(outer_tx, vec![inner_tx1, inner_tx2]);
813 let result =
814 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 1, 2]))
815 .unwrap();
816
817 assert_eq!(result.len(), 2);
818 assert!(result[0].tx_logs.is_some());
819 assert!(result[0].bundle_logs.is_none());
820 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
821
822 assert!(result[1].tx_logs.is_none());
823 assert!(result[1].bundle_logs.is_some());
824
825 let nested_logs = result[1].bundle_logs.as_ref().unwrap();
826 assert_eq!(nested_logs.len(), 2);
827 assert!(nested_logs[0].tx_logs.is_some());
828 assert_eq!(nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
829 assert!(nested_logs[1].tx_logs.is_some());
830 assert_eq!(nested_logs[1].tx_logs.as_ref().unwrap().len(), 2);
831 }
832
833 #[test]
834 fn test_build_bundle_logs_duplicate_transactions_same_level() {
835 let duplicate_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
836 let bundle = create_test_bundle(vec![duplicate_tx.clone(), duplicate_tx]);
837 let result =
838 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 2])).unwrap();
839
840 assert_eq!(result.len(), 2);
841 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
842 assert_eq!(result[1].tx_logs.as_ref().unwrap().len(), 2);
843 }
844
845 #[test]
846 fn test_build_bundle_logs_duplicate_transactions_across_nested_bundles() {
847 let duplicate_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
848 let bundle = create_nested_bundle(duplicate_tx.clone(), vec![duplicate_tx]);
849 let result =
850 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 2])).unwrap();
851
852 assert_eq!(result.len(), 2);
853 assert!(result[1].bundle_logs.is_some());
854 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
855
856 let nested_logs = result[1].bundle_logs.as_ref().unwrap();
857 assert_eq!(nested_logs.len(), 1);
858 assert_eq!(nested_logs[0].tx_logs.as_ref().unwrap().len(), 2);
859 }
860
861 #[test]
862 fn test_build_bundle_logs_root_with_only_nested_bundles() {
863 let first_nested = create_test_bundle(vec![Bytes::from(vec![0x01])]);
864 let second_nested =
865 create_test_bundle(vec![Bytes::from(vec![0x02]), Bytes::from(vec![0x03])]);
866 let bundle = create_bundle_with_body(vec![
867 BundleItem::Bundle { bundle: first_nested },
868 BundleItem::Bundle { bundle: second_nested },
869 ]);
870 let result =
871 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 1, 2]))
872 .unwrap();
873
874 assert_eq!(result.len(), 2);
875 assert!(result[0].tx_logs.is_none());
876 assert!(result[1].tx_logs.is_none());
877
878 let first_nested_logs = result[0].bundle_logs.as_ref().unwrap();
879 assert_eq!(first_nested_logs.len(), 1);
880 assert_eq!(first_nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
881
882 let second_nested_logs = result[1].bundle_logs.as_ref().unwrap();
883 assert_eq!(second_nested_logs.len(), 2);
884 assert_eq!(second_nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
885 assert_eq!(second_nested_logs[1].tx_logs.as_ref().unwrap().len(), 2);
886 }
887
888 #[test]
889 fn test_build_bundle_logs_deeply_nested_bundle() {
890 let leaf_bundle = create_test_bundle(vec![Bytes::from(vec![0x03])]);
891 let middle_bundle = create_bundle_with_body(vec![
892 BundleItem::Tx { tx: Bytes::from(vec![0x02]), can_revert: false },
893 BundleItem::Bundle { bundle: leaf_bundle },
894 ]);
895 let root_bundle = create_bundle_with_body(vec![
896 BundleItem::Tx { tx: Bytes::from(vec![0x01]), can_revert: false },
897 BundleItem::Bundle { bundle: middle_bundle },
898 ]);
899 let result =
900 EthSimBundle::<()>::build_bundle_logs(&root_bundle, &create_bundle_logs(&[1, 2, 3]))
901 .unwrap();
902
903 assert_eq!(result.len(), 2);
904 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
905
906 let middle_logs = result[1].bundle_logs.as_ref().unwrap();
907 assert_eq!(middle_logs.len(), 2);
908 assert_eq!(middle_logs[0].tx_logs.as_ref().unwrap().len(), 2);
909
910 let leaf_logs = middle_logs[1].bundle_logs.as_ref().unwrap();
911 assert_eq!(leaf_logs.len(), 1);
912 assert_eq!(leaf_logs[0].tx_logs.as_ref().unwrap().len(), 3);
913 }
914
915 #[test]
916 fn test_build_bundle_logs_mismatched_flat_logs() {
917 let bundle = create_test_bundle(vec![Bytes::from(vec![0x01, 0x02, 0x03])]);
918
919 assert_unmatched_bundle(EthSimBundle::<()>::build_bundle_logs(&bundle, &[]));
920 assert_unmatched_bundle(EthSimBundle::<()>::build_bundle_logs(
921 &bundle,
922 &create_bundle_logs(&[1, 2]),
923 ));
924 }
925
926 #[tokio::test(flavor = "multi_thread")]
929 async fn sim_bundle_caps_gas_at_call_gas_limit() {
930 let mut rng = generators::rng();
931 let key = generate_key(&mut rng);
932 let txs: Vec<_> = (0..2)
933 .map(|nonce| {
934 sign_tx_with_key_pair(
935 key,
936 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
937 chain_id: 1,
938 nonce,
939 gas_limit: 21_000,
940 max_fee_per_gas: INITIAL_BASE_FEE as u128,
941 to: TxKind::Call(Address::random()),
942 ..Default::default()
943 }),
944 )
945 })
946 .collect();
947 let sender = txs[0].recover_signer().unwrap();
948
949 let genesis = Genesis::default()
950 .with_gas_limit(30_000_000)
951 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
952 .extend_accounts([(
953 sender,
954 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
955 )]);
956 let chain_spec =
957 Arc::new(ChainSpecBuilder::mainnet().cancun_activated().genesis(genesis).build());
958 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
959 init_genesis(&factory).unwrap();
960 let provider = BlockchainProvider::new(factory).unwrap();
961 let api = |gas_cap: u64| {
962 let eth_api = EthApiBuilder::new(
963 provider.clone(),
964 testing_pool(),
965 NoopNetwork::default(),
966 EthEvmConfig::new(provider.chain_spec()),
967 )
968 .gas_cap(gas_cap.into())
969 .build();
970 EthSimBundle::new(eth_api, BlockingTaskGuard::new(4))
971 };
972 let bundle = create_test_bundle(txs.iter().map(|tx| tx.encoded_2718().into()).collect());
973 let overrides = SimBundleOverrides {
974 block_overrides: BlockOverrides { gas_limit: Some(21_001), ..Default::default() },
975 ..Default::default()
976 };
977
978 let capped = api(21_000);
979 assert_eq!(
980 capped.sim_bundle(bundle.clone(), overrides).await.unwrap_err(),
981 ErrorObjectOwned::from(EthApiError::InvalidTransaction(
982 RpcInvalidTransactionError::GasTooHigh
983 ))
984 );
985 assert_eq!(
986 capped.sim_bundle(bundle.clone(), SimBundleOverrides::default()).await.unwrap_err(),
987 ErrorObjectOwned::from(EthApiError::InvalidParams(
988 EthBundleError::BundleGasLimitExceeded(21_000).to_string()
989 ))
990 );
991 let response = api(0).sim_bundle(bundle, SimBundleOverrides::default()).await.unwrap();
992 assert_eq!(response.gas_used, 42_000);
993 }
994}