1use alloy_consensus::{transaction::TxHashRef, BlockHeader};
4use alloy_eips::BlockNumberOrTag;
5use alloy_evm::{env::BlockEnvironment, overrides::apply_block_overrides};
6use alloy_primitives::U256;
7use alloy_rpc_types_eth::{BlockId, Log};
8use alloy_rpc_types_mev::{
9 BundleItem, Inclusion, MevSendBundle, Privacy, RefundConfig, SimBundleLogs, SimBundleOverrides,
10 SimBundleResponse, Validity,
11};
12use jsonrpsee::core::RpcResult;
13use reth_evm::{ConfigureEvm, Evm};
14use reth_primitives_traits::Recovered;
15use reth_rpc_api::MevSimApiServer;
16use reth_rpc_eth_api::{
17 helpers::{block::LoadBlock, Call, EthTransactions},
18 FromEthApiError, FromEvmError,
19};
20use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError};
21use reth_storage_api::ProviderTx;
22use reth_tasks::pool::BlockingTaskGuard;
23use reth_transaction_pool::{PoolPooledTx, PoolTransaction, TransactionPool};
24use revm::{
25 context::Block, context_interface::result::ResultAndState, DatabaseCommit, DatabaseRef,
26};
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
36const DEFAULT_SIM_TIMEOUT: Duration = Duration::from_secs(5);
38
39const 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 + 'static,
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 = recover_raw_transaction::<PoolPooledTx<Eth::Pool>>(tx)?;
232 let tx = recovered_tx.map(
233 <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
234 );
235
236 let refund_percent =
237 validity.as_ref().and_then(|v| v.refund.as_ref()).and_then(|refunds| {
238 refunds.iter().find_map(|refund| {
239 (refund.body_idx as usize == idx).then_some(refund.percent)
240 })
241 });
242 let refund_configs =
243 validity.as_ref().and_then(|v| v.refund_config.clone());
244
245 let flattened_item = FlattenedBundleItem {
247 tx,
248 can_revert: *can_revert,
249 inclusion: inclusion.clone(),
250 validity: validity.clone(),
251 privacy: privacy.clone(),
252 refund_percent,
253 refund_configs,
254 };
255
256 items.push(flattened_item);
257 idx += 1;
258 }
259 BundleItem::Bundle { bundle } => {
260 stack.push((current_bundle, idx + 1, depth));
262
263 stack.push((bundle, 0, depth + 1));
265 break;
266 }
267 BundleItem::Hash { hash: _ } => {
268 return Err(EthApiError::InvalidParams(
270 EthSimBundleError::InvalidBundle.to_string(),
271 ));
272 }
273 }
274 }
275 }
276
277 Ok(items)
278 }
279
280 async fn sim_bundle_inner(
281 &self,
282 request: MevSendBundle,
283 overrides: SimBundleOverrides,
284 logs: bool,
285 ) -> Result<SimBundleResponse, Eth::Error> {
286 let SimBundleOverrides { parent_block, block_overrides, .. } = overrides;
287
288 let flattened_bundle = self.parse_and_flatten_bundle(&request)?;
291
292 let block_id = parent_block.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest));
293 let (mut evm_env, current_block_id) = self.eth_api().evm_env_at(block_id).await?;
294 let current_block = self.eth_api().recovered_block(current_block_id).await?;
295 let current_block = current_block.ok_or(EthApiError::HeaderNotFound(block_id))?;
296
297 let eth_api = self.inner.eth_api.clone();
298
299 let sim_response = self
300 .inner
301 .eth_api
302 .spawn_with_state_at_block(current_block_id, move |_, mut db| {
303 let current_block_number = current_block.number();
305 let coinbase = evm_env.block_env.beneficiary();
306 let basefee = evm_env.block_env.basefee();
307
308 apply_block_overrides(block_overrides, &mut db, evm_env.block_env.inner_mut());
310
311 let initial_coinbase_balance = DatabaseRef::basic_ref(&db, coinbase)
312 .map_err(EthApiError::from_eth_err)?
313 .map(|acc| acc.balance)
314 .unwrap_or_default();
315
316 let mut coinbase_balance_before_tx = initial_coinbase_balance;
317 let mut total_gas_used = 0;
318 let mut total_profit = U256::ZERO;
319 let mut refundable_value = U256::ZERO;
320 let mut flat_logs: Vec<Vec<Log>> = Vec::new();
321
322 let mut evm = eth_api.evm_config().evm_with_env(db, evm_env);
323 let mut log_index = 0;
324
325 for (tx_index, item) in flattened_bundle.iter().enumerate() {
326 let block_number = item.inclusion.block_number();
328 let max_block_number =
329 item.inclusion.max_block_number().unwrap_or(block_number);
330
331 if current_block_number < block_number ||
332 current_block_number > max_block_number
333 {
334 return Err(EthApiError::InvalidParams(
335 EthSimBundleError::InvalidInclusion.to_string(),
336 )
337 .into());
338 }
339
340 let ResultAndState { result, state } = evm
341 .transact(eth_api.evm_config().tx_env(&item.tx))
342 .map_err(Eth::Error::from_evm_err)?;
343
344 if !result.is_success() && !item.can_revert {
345 return Err(EthApiError::InvalidParams(
346 EthSimBundleError::BundleTransactionFailed.to_string(),
347 )
348 .into());
349 }
350
351 let gas_used = result.gas_used();
352 total_gas_used += gas_used;
353
354 let coinbase_balance_after_tx =
356 state.get(&coinbase).map(|acc| acc.info.balance).unwrap_or_default();
357
358 let coinbase_diff =
359 coinbase_balance_after_tx.saturating_sub(coinbase_balance_before_tx);
360 total_profit += coinbase_diff;
361
362 if item.refund_percent.is_none() {
364 refundable_value += coinbase_diff;
365 }
366
367 coinbase_balance_before_tx = coinbase_balance_after_tx;
369
370 if logs {
373 let tx_logs: Vec<Log> = result
374 .into_logs()
375 .into_iter()
376 .map(|inner| {
377 let full_log = Log {
378 inner,
379 block_hash: Some(current_block.hash()),
380 block_number: Some(current_block.number()),
381 block_timestamp: Some(current_block.timestamp()),
382 transaction_hash: Some(*item.tx.tx_hash()),
383 transaction_index: Some(tx_index as u64),
384 log_index: Some(log_index),
385 removed: false,
386 };
387 log_index += 1;
388 full_log
389 })
390 .collect();
391 flat_logs.push(tx_logs);
392 }
393
394 evm.db_mut().commit(state);
396 }
397
398 let body_logs =
399 if logs { Self::build_bundle_logs(&request, &flat_logs)? } else { vec![] };
400
401 let original_refundable_value = refundable_value;
404 for item in &flattened_bundle {
405 if let Some(refund_percent) = item.refund_percent {
406 let refund_configs = item.refund_configs.clone().unwrap_or_else(|| {
407 vec![RefundConfig { address: item.tx.signer(), percent: 100 }]
408 });
409
410 let payout_tx_fee = U256::from(basefee) *
412 U256::from(SBUNDLE_PAYOUT_MAX_COST) *
413 U256::from(refund_configs.len() as u64);
414
415 total_gas_used += SBUNDLE_PAYOUT_MAX_COST * refund_configs.len() as u64;
417
418 let payout_value = original_refundable_value * U256::from(refund_percent) /
422 U256::from(100);
423
424 if payout_tx_fee > payout_value {
425 return Err(EthApiError::InvalidParams(
426 EthSimBundleError::NegativeProfit.to_string(),
427 )
428 .into());
429 }
430
431 total_profit = total_profit.checked_sub(payout_value).ok_or(
433 EthApiError::InvalidParams(
434 EthSimBundleError::NegativeProfit.to_string(),
435 ),
436 )?;
437
438 refundable_value = refundable_value.checked_sub(payout_value).ok_or(
440 EthApiError::InvalidParams(
441 EthSimBundleError::NegativeProfit.to_string(),
442 ),
443 )?;
444 }
445 }
446
447 let mev_gas_price = if total_gas_used != 0 {
449 total_profit / U256::from(total_gas_used)
450 } else {
451 U256::ZERO
452 };
453
454 Ok(SimBundleResponse {
455 success: true,
456 state_block: current_block_number,
457 error: None,
458 logs: Some(body_logs),
459 gas_used: total_gas_used,
460 mev_gas_price,
461 profit: total_profit,
462 refundable_value,
463 exec_error: None,
464 revert: None,
465 })
466 })
467 .await?;
468
469 Ok(sim_response)
470 }
471}
472
473#[async_trait::async_trait]
474impl<Eth> MevSimApiServer for EthSimBundle<Eth>
475where
476 Eth: EthTransactions + LoadBlock + Call + 'static,
477{
478 async fn sim_bundle(
479 &self,
480 request: MevSendBundle,
481 overrides: SimBundleOverrides,
482 ) -> RpcResult<SimBundleResponse> {
483 trace!("mev_simBundle called, request: {:?}, overrides: {:?}", request, overrides);
484
485 let override_timeout = overrides.timeout;
486
487 let timeout = override_timeout
488 .map(Duration::from_secs)
489 .map(|d| d.min(MAX_SIM_TIMEOUT))
490 .unwrap_or(DEFAULT_SIM_TIMEOUT);
491
492 let bundle_res =
493 tokio::time::timeout(timeout, Self::sim_bundle_inner(self, request, overrides, true))
494 .await
495 .map_err(|_| {
496 EthApiError::InvalidParams(EthSimBundleError::BundleTimeout.to_string())
497 })?;
498
499 bundle_res.map_err(Into::into)
500 }
501}
502
503#[derive(Debug)]
505struct EthSimBundleInner<Eth> {
506 eth_api: Eth,
508 #[expect(dead_code)]
510 blocking_task_guard: BlockingTaskGuard,
511}
512
513impl<Eth> std::fmt::Debug for EthSimBundle<Eth> {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 f.debug_struct("EthSimBundle").finish_non_exhaustive()
516 }
517}
518
519impl<Eth> Clone for EthSimBundle<Eth> {
520 fn clone(&self) -> Self {
521 Self { inner: Arc::clone(&self.inner) }
522 }
523}
524
525#[derive(Debug, thiserror::Error)]
527pub enum EthSimBundleError {
528 #[error("max depth reached")]
530 MaxDepth,
531 #[error("unmatched bundle")]
533 UnmatchedBundle,
534 #[error("bundle too large")]
536 BundleTooLarge,
537 #[error("invalid validity")]
539 InvalidValidity,
540 #[error("invalid inclusion")]
542 InvalidInclusion,
543 #[error("invalid bundle")]
545 InvalidBundle,
546 #[error("bundle simulation timed out")]
548 BundleTimeout,
549 #[error("bundle transaction failed")]
551 BundleTransactionFailed,
552 #[error("bundle simulation returned negative profit")]
554 NegativeProfit,
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use alloy_primitives::Bytes;
561 use alloy_rpc_types_mev::{Inclusion, ProtocolVersion};
562
563 fn create_test_bundle(tx_bytes: Vec<Bytes>) -> MevSendBundle {
564 let body: Vec<BundleItem> =
565 tx_bytes.into_iter().map(|tx| BundleItem::Tx { tx, can_revert: false }).collect();
566 MevSendBundle {
567 bundle_body: body,
568 inclusion: Inclusion { block: 1, max_block: None },
569 validity: None,
570 privacy: None,
571 protocol_version: ProtocolVersion::V0_1,
572 }
573 }
574
575 fn create_nested_bundle(outer_tx: Bytes, inner_txs: Vec<Bytes>) -> MevSendBundle {
576 let inner_bundle = create_test_bundle(inner_txs);
577 MevSendBundle {
578 bundle_body: vec![
579 BundleItem::Tx { tx: outer_tx, can_revert: false },
580 BundleItem::Bundle { bundle: inner_bundle },
581 ],
582 inclusion: Inclusion { block: 1, max_block: None },
583 validity: None,
584 privacy: None,
585 protocol_version: ProtocolVersion::V0_1,
586 }
587 }
588
589 fn create_bundle_with_body(bundle_body: Vec<BundleItem>) -> MevSendBundle {
590 MevSendBundle {
591 bundle_body,
592 inclusion: Inclusion { block: 1, max_block: None },
593 validity: None,
594 privacy: None,
595 protocol_version: ProtocolVersion::V0_1,
596 }
597 }
598
599 fn create_bundle_logs(log_counts: &[usize]) -> Vec<Vec<Log>> {
600 log_counts.iter().map(|count| vec![Log::default(); *count]).collect()
601 }
602
603 fn assert_unmatched_bundle(result: Result<Vec<SimBundleLogs>, EthApiError>) {
604 assert!(matches!(
605 result,
606 Err(EthApiError::InvalidParams(ref message))
607 if message == &EthSimBundleError::UnmatchedBundle.to_string()
608 ));
609 }
610
611 #[test]
612 fn test_build_bundle_logs_single_tx() {
613 let bundle = create_test_bundle(vec![Bytes::from(vec![0x01, 0x02, 0x03])]);
614 let result =
615 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1])).unwrap();
616
617 assert_eq!(result.len(), 1);
618 assert!(result[0].tx_logs.is_some());
619 assert!(result[0].bundle_logs.is_none());
620 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
621 }
622
623 #[test]
624 fn test_build_bundle_logs_empty_bundle() {
625 let bundle = create_test_bundle(vec![]);
626 let result = EthSimBundle::<()>::build_bundle_logs(&bundle, &[]).unwrap();
627
628 assert!(result.is_empty());
629 }
630
631 #[test]
632 fn test_build_bundle_logs_nested_bundle() {
633 let outer_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
634 let inner_tx1 = Bytes::from(vec![0x04, 0x05, 0x06]);
635 let inner_tx2 = Bytes::from(vec![0x07, 0x08, 0x09]);
636 let bundle = create_nested_bundle(outer_tx, vec![inner_tx1, inner_tx2]);
637 let result =
638 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 1, 2]))
639 .unwrap();
640
641 assert_eq!(result.len(), 2);
642 assert!(result[0].tx_logs.is_some());
643 assert!(result[0].bundle_logs.is_none());
644 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
645
646 assert!(result[1].tx_logs.is_none());
647 assert!(result[1].bundle_logs.is_some());
648
649 let nested_logs = result[1].bundle_logs.as_ref().unwrap();
650 assert_eq!(nested_logs.len(), 2);
651 assert!(nested_logs[0].tx_logs.is_some());
652 assert_eq!(nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
653 assert!(nested_logs[1].tx_logs.is_some());
654 assert_eq!(nested_logs[1].tx_logs.as_ref().unwrap().len(), 2);
655 }
656
657 #[test]
658 fn test_build_bundle_logs_duplicate_transactions_same_level() {
659 let duplicate_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
660 let bundle = create_test_bundle(vec![duplicate_tx.clone(), duplicate_tx]);
661 let result =
662 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 2])).unwrap();
663
664 assert_eq!(result.len(), 2);
665 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
666 assert_eq!(result[1].tx_logs.as_ref().unwrap().len(), 2);
667 }
668
669 #[test]
670 fn test_build_bundle_logs_duplicate_transactions_across_nested_bundles() {
671 let duplicate_tx = Bytes::from(vec![0x01, 0x02, 0x03]);
672 let bundle = create_nested_bundle(duplicate_tx.clone(), vec![duplicate_tx]);
673 let result =
674 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 2])).unwrap();
675
676 assert_eq!(result.len(), 2);
677 assert!(result[1].bundle_logs.is_some());
678 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
679
680 let nested_logs = result[1].bundle_logs.as_ref().unwrap();
681 assert_eq!(nested_logs.len(), 1);
682 assert_eq!(nested_logs[0].tx_logs.as_ref().unwrap().len(), 2);
683 }
684
685 #[test]
686 fn test_build_bundle_logs_root_with_only_nested_bundles() {
687 let first_nested = create_test_bundle(vec![Bytes::from(vec![0x01])]);
688 let second_nested =
689 create_test_bundle(vec![Bytes::from(vec![0x02]), Bytes::from(vec![0x03])]);
690 let bundle = create_bundle_with_body(vec![
691 BundleItem::Bundle { bundle: first_nested },
692 BundleItem::Bundle { bundle: second_nested },
693 ]);
694 let result =
695 EthSimBundle::<()>::build_bundle_logs(&bundle, &create_bundle_logs(&[1, 1, 2]))
696 .unwrap();
697
698 assert_eq!(result.len(), 2);
699 assert!(result[0].tx_logs.is_none());
700 assert!(result[1].tx_logs.is_none());
701
702 let first_nested_logs = result[0].bundle_logs.as_ref().unwrap();
703 assert_eq!(first_nested_logs.len(), 1);
704 assert_eq!(first_nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
705
706 let second_nested_logs = result[1].bundle_logs.as_ref().unwrap();
707 assert_eq!(second_nested_logs.len(), 2);
708 assert_eq!(second_nested_logs[0].tx_logs.as_ref().unwrap().len(), 1);
709 assert_eq!(second_nested_logs[1].tx_logs.as_ref().unwrap().len(), 2);
710 }
711
712 #[test]
713 fn test_build_bundle_logs_deeply_nested_bundle() {
714 let leaf_bundle = create_test_bundle(vec![Bytes::from(vec![0x03])]);
715 let middle_bundle = create_bundle_with_body(vec![
716 BundleItem::Tx { tx: Bytes::from(vec![0x02]), can_revert: false },
717 BundleItem::Bundle { bundle: leaf_bundle },
718 ]);
719 let root_bundle = create_bundle_with_body(vec![
720 BundleItem::Tx { tx: Bytes::from(vec![0x01]), can_revert: false },
721 BundleItem::Bundle { bundle: middle_bundle },
722 ]);
723 let result =
724 EthSimBundle::<()>::build_bundle_logs(&root_bundle, &create_bundle_logs(&[1, 2, 3]))
725 .unwrap();
726
727 assert_eq!(result.len(), 2);
728 assert_eq!(result[0].tx_logs.as_ref().unwrap().len(), 1);
729
730 let middle_logs = result[1].bundle_logs.as_ref().unwrap();
731 assert_eq!(middle_logs.len(), 2);
732 assert_eq!(middle_logs[0].tx_logs.as_ref().unwrap().len(), 2);
733
734 let leaf_logs = middle_logs[1].bundle_logs.as_ref().unwrap();
735 assert_eq!(leaf_logs.len(), 1);
736 assert_eq!(leaf_logs[0].tx_logs.as_ref().unwrap().len(), 3);
737 }
738
739 #[test]
740 fn test_build_bundle_logs_mismatched_flat_logs() {
741 let bundle = create_test_bundle(vec![Bytes::from(vec![0x01, 0x02, 0x03])]);
742
743 assert_unmatched_bundle(EthSimBundle::<()>::build_bundle_logs(&bundle, &[]));
744 assert_unmatched_bundle(EthSimBundle::<()>::build_bundle_logs(
745 &bundle,
746 &create_bundle_logs(&[1, 2]),
747 ));
748 }
749}