1use crate::eth::sim_bundle::{DEFAULT_SIM_TIMEOUT, MAX_SIM_TIMEOUT};
4use alloy_consensus::{transaction::TxHashRef, EnvKzgSettings, Transaction as _};
5use alloy_eips::eip7840::BlobParams;
6use alloy_primitives::{uint, Keccak256, U256};
7use alloy_rpc_types_mev::{EthCallBundle, EthCallBundleResponse, EthCallBundleTransactionResult};
8use evm2::evm::DynDatabase;
9use jsonrpsee::core::RpcResult;
10use reth_chainspec::{ChainSpecProvider, EthChainSpec};
11use reth_evm::{ConfigureEvm, Evm, EvmEnv};
12use reth_rpc_eth_api::{
13 helpers::{Call, EthTransactions},
14 EthCallBundleApiServer, FromEthApiError, FromEvmError,
15};
16use reth_rpc_eth_types::{
17 EthApiError, PendingBlockEnv, PendingBlockEnvOrigin, RpcInvalidTransactionError,
18};
19use reth_storage_api::BlockReaderIdExt;
20use reth_tasks::{cancel::is_cancelled, pool::BlockingTaskGuard};
21use reth_transaction_pool::{
22 EthBlobTransactionSidecar, EthPoolTransaction, PoolPooledTx, PoolTransaction, TransactionPool,
23};
24use std::{
25 sync::Arc,
26 time::{Duration, Instant},
27};
28
29pub struct EthBundle<Eth> {
31 inner: Arc<EthBundleInner<Eth>>,
33}
34
35impl<Eth> EthBundle<Eth> {
36 pub fn new(eth_api: Eth, blocking_task_guard: BlockingTaskGuard) -> Self {
38 Self { inner: Arc::new(EthBundleInner { eth_api, blocking_task_guard }) }
39 }
40
41 pub fn eth_api(&self) -> &Eth {
43 &self.inner.eth_api
44 }
45}
46
47impl<Eth> EthBundle<Eth>
48where
49 Eth: EthTransactions + Call,
50{
51 pub async fn call_bundle(
60 &self,
61 bundle: EthCallBundle,
62 ) -> Result<EthCallBundleResponse, Eth::Error> {
63 let EthCallBundle {
64 txs,
65 block_number,
66 coinbase,
67 state_block_number,
68 timeout,
69 timestamp,
70 gas_limit,
71 difficulty,
72 base_fee,
73 ..
74 } = bundle;
75 if txs.is_empty() {
76 return Err(EthApiError::InvalidParams(
77 EthBundleError::EmptyBundleTransactions.to_string(),
78 )
79 .into())
80 }
81 if block_number == 0 {
82 return Err(EthApiError::InvalidParams(
83 EthBundleError::BundleMissingBlockNumber.to_string(),
84 )
85 .into())
86 }
87
88 let call_gas_limit = self.inner.eth_api.call_gas_limit();
90 if let Some(gas_limit) = gas_limit &&
91 gas_limit > call_gas_limit
92 {
93 return Err(
94 EthApiError::InvalidTransaction(RpcInvalidTransactionError::GasTooHigh).into()
95 )
96 }
97
98 let timeout = timeout
100 .filter(|timeout| *timeout > 0)
101 .map(Duration::from_millis)
102 .map_or(DEFAULT_SIM_TIMEOUT, |timeout| timeout.min(MAX_SIM_TIMEOUT));
103
104 let transactions =
105 self.eth_api().recover_raw_transactions::<PoolPooledTx<Eth::Pool>>(txs)?;
106
107 let block_id: alloy_rpc_types_eth::BlockId = state_block_number.into();
108 let (mut evm_env, at, parent) = if block_id.is_pending() {
110 let PendingBlockEnv { evm_env, origin } = self.eth_api().pending_block_env_and_cfg()?;
111 let at = origin.state_block_id();
112 let parent = match origin {
113 PendingBlockEnvOrigin::ActualPending(block, _) => block.clone_sealed_header(),
114 PendingBlockEnvOrigin::DerivedFromLatest(header) => header,
115 };
116 (evm_env, at, parent)
117 } else {
118 let parent = self
119 .eth_api()
120 .provider()
121 .sealed_header_by_id(block_id)
122 .map_err(Eth::Error::from_eth_err)?
123 .ok_or(EthApiError::HeaderNotFound(block_id))?;
124 let evm_env = self.eth_api().evm_env_for_header(&parent)?;
125 (evm_env, parent.hash().into(), parent)
126 };
127
128 if let Some(coinbase) = coinbase {
129 evm_env.block_env_mut().beneficiary = coinbase;
130 }
131
132 if let Some(timestamp) = timestamp {
134 evm_env.block_env_mut().timestamp = U256::from(timestamp);
135 } else {
136 evm_env.block_env_mut().timestamp += uint!(12_U256);
137 }
138
139 if let Some(difficulty) = difficulty {
140 evm_env.block_env_mut().difficulty = difficulty;
141 }
142
143 let blob_gas_used = transactions.iter().filter_map(|tx| tx.blob_gas_used()).sum::<u64>();
146 if blob_gas_used > 0 {
147 let blob_params = self
148 .eth_api()
149 .provider()
150 .chain_spec()
151 .blob_params_at_timestamp(evm_env.block_env().timestamp.saturating_to())
152 .unwrap_or_else(BlobParams::cancun);
153 if blob_gas_used > blob_params.max_blob_gas_per_block() {
154 return Err(EthApiError::InvalidParams(
155 EthBundleError::Eip4844BlobGasExceeded(blob_params.max_blob_gas_per_block())
156 .to_string(),
157 )
158 .into())
159 }
160 }
161
162 let block_gas_limit = gas_limit.unwrap_or(call_gas_limit);
164 evm_env.block_env_mut().gas_limit = U256::from(block_gas_limit);
165
166 if let Some(base_fee) = base_fee {
169 evm_env.block_env_mut().basefee = U256::from(base_fee.min(u64::MAX as u128));
170 } else if let Some(next_base_fee) = self
171 .eth_api()
172 .provider()
173 .chain_spec()
174 .next_block_base_fee(&parent, evm_env.block_env().timestamp.saturating_to())
175 {
176 evm_env.block_env_mut().basefee = U256::from(next_base_fee);
177 }
178
179 let state_block_number = evm_env.block_env().number;
180 evm_env.block_env_mut().number = U256::from(block_number);
182
183 let permit = self
184 .eth_api()
185 .acquire_owned_blocking_io()
186 .await
187 .map_err(|_| EthApiError::InternalEthError)?;
188
189 self.eth_api()
190 .spawn_with_state_at_block(at, move |eth_api, mut db| {
191 let _permit = permit;
192 let coinbase = evm_env.block_env().beneficiary;
193 let basefee = evm_env.block_base_fee();
194
195 let initial_coinbase = db
196 .get_account(&coinbase)
197 .map_err(Eth::Error::from_eth_err)?
198 .map(|acc| acc.balance)
199 .unwrap_or_default();
200 let mut block_gas = BundleBlockGas::new(block_gas_limit, evm_env.version());
201 let mut evm = eth_api.evm_config().evm_with_env(&mut db, evm_env);
202 let mut coinbase_balance_before_tx = initial_coinbase;
203 let mut coinbase_balance_after_tx = initial_coinbase;
204 let mut total_gas_fees = U256::ZERO;
205 let mut hasher = Keccak256::new();
206
207 let mut results = Vec::with_capacity(transactions.len());
208 let deadline = Instant::now() + timeout;
210 for tx in transactions {
211 if is_cancelled() {
212 return Err(EthApiError::InternalEthError.into())
213 }
214
215 if Instant::now() >= deadline {
216 return Err(EthApiError::ExecutionTimedOut(timeout).into())
217 }
218
219 if !block_gas.admits(tx.gas_limit()) {
220 return Err(EthApiError::InvalidParams(
221 EthBundleError::BundleGasLimitExceeded(block_gas_limit).to_string(),
222 )
223 .into())
224 }
225
226 let signer = tx.signer();
227 let tx = {
228 let mut tx = <Eth::Pool as TransactionPool>::Transaction::from_pooled(tx);
229
230 if let EthBlobTransactionSidecar::Present(sidecar) = tx.take_blob() {
231 tx.validate_blob(&sidecar, EnvKzgSettings::Default.get()).map_err(
232 |e| {
233 Eth::Error::from_eth_err(EthApiError::InvalidParams(
234 e.to_string(),
235 ))
236 },
237 )?;
238 }
239
240 tx.into_consensus()
241 };
242
243 let tx_hash = *tx.tx_hash();
244 hasher.update(tx_hash);
245 let gas_price = tx.effective_tip_per_gas(basefee).ok_or_else(|| {
247 Eth::Error::from_eth_err(EthApiError::InvalidTransaction(
248 RpcInvalidTransactionError::FeeCapTooLow,
249 ))
250 })?;
251 let to_address = tx.inner().to();
252 let tx_env = eth_api.evm_config().tx_env(tx);
253 let result = evm.transact_commit(&tx_env).map_err(Eth::Error::from_evm_err)?;
254
255 let gas_used = result.tx_gas_used();
256 block_gas.record(&result);
257
258 let gas_fees = U256::from(gas_used) * U256::from(gas_price);
259 total_gas_fees += gas_fees;
260
261 coinbase_balance_after_tx = evm
262 .account_info(&coinbase)
263 .map_err(Eth::Error::from_evm_err)?
264 .map(|acc| acc.balance)
265 .unwrap_or_default();
266 let coinbase_diff =
267 coinbase_balance_after_tx.saturating_sub(coinbase_balance_before_tx);
268 let eth_sent_to_coinbase = coinbase_diff.saturating_sub(gas_fees);
269
270 coinbase_balance_before_tx = coinbase_balance_after_tx;
272
273 let (value, revert) = if result.status {
275 let value = result.output;
276 (Some(value), None)
277 } else {
278 let revert = result.output;
279 (None, Some(revert))
280 };
281
282 let tx_res = EthCallBundleTransactionResult {
283 coinbase_diff,
284 eth_sent_to_coinbase,
285 from_address: signer,
286 gas_fees,
287 gas_price: U256::from(gas_price),
288 gas_used,
289 to_address,
290 tx_hash,
291 value,
292 revert,
293 };
294 results.push(tx_res);
295 }
296
297 let total_gas_used = block_gas.gas_used();
300 let coinbase_diff = coinbase_balance_after_tx.saturating_sub(initial_coinbase);
301 let eth_sent_to_coinbase = coinbase_diff.saturating_sub(total_gas_fees);
302 let bundle_gas_price =
303 coinbase_diff.checked_div(U256::from(total_gas_used)).unwrap_or_default();
304 let res = EthCallBundleResponse {
305 bundle_gas_price,
306 bundle_hash: hasher.finalize(),
307 coinbase_diff,
308 eth_sent_to_coinbase,
309 gas_fees: total_gas_fees,
310 results,
311 state_block_number: state_block_number.to(),
312 total_gas_used,
313 };
314
315 Ok(res)
316 })
317 .await
318 }
319}
320
321impl<Eth> EthCallBundleApiServer for EthBundle<Eth>
322where
323 Eth: EthTransactions + Call,
324{
325 async fn call_bundle(&self, request: EthCallBundle) -> RpcResult<EthCallBundleResponse> {
326 Self::call_bundle(self, request).await.map_err(Into::into)
327 }
328}
329
330#[derive(Debug)]
332struct EthBundleInner<Eth> {
333 eth_api: Eth,
335 #[expect(dead_code)]
337 blocking_task_guard: BlockingTaskGuard,
338}
339
340impl<Eth> std::fmt::Debug for EthBundle<Eth> {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("EthBundle").finish_non_exhaustive()
343 }
344}
345
346impl<Eth> Clone for EthBundle<Eth> {
347 fn clone(&self) -> Self {
348 Self { inner: Arc::clone(&self.inner) }
349 }
350}
351
352#[derive(Debug, thiserror::Error)]
354pub enum EthBundleError {
355 #[error("bundle missing txs")]
357 EmptyBundleTransactions,
358 #[error("bundle missing blockNumber")]
360 BundleMissingBlockNumber,
361 #[error("blob gas usage exceeds the limit of {0} gas per block.")]
363 Eip4844BlobGasExceeded(u64),
364 #[error("bundle gas exceeds the block gas limit of {0}")]
366 BundleGasLimitExceeded(u64),
367}
368
369#[derive(Debug)]
374pub(crate) struct BundleBlockGas {
375 limit: u64,
376 tx_gas_limit_cap: u64,
377 separate_pools: bool,
378 gas_used: u64,
379 regular_gas_used: u64,
380 state_gas_used: u64,
381}
382
383impl BundleBlockGas {
384 pub(crate) const fn new(limit: u64, version: &evm2::Version) -> Self {
386 Self {
387 limit,
388 tx_gas_limit_cap: version.tx_gas_limit_cap,
389 separate_pools: version.feature(evm2::EvmFeatures::EIP8037),
390 gas_used: 0,
391 regular_gas_used: 0,
392 state_gas_used: 0,
393 }
394 }
395
396 pub(crate) const fn gas_used(&self) -> u64 {
398 self.gas_used
399 }
400
401 pub(crate) fn admits(&self, gas_limit: u64) -> bool {
403 let regular_limit = gas_limit.min(self.tx_gas_limit_cap);
404 if self.separate_pools {
405 regular_limit <= self.limit.saturating_sub(self.regular_gas_used) &&
406 gas_limit <= self.limit.saturating_sub(self.state_gas_used)
407 } else {
408 regular_limit <= self.limit.saturating_sub(self.gas_used)
409 }
410 }
411
412 pub(crate) const fn record<E>(&mut self, result: &evm2::TxResultExt<E>) {
414 self.gas_used += result.tx_gas_used();
415 self.regular_gas_used += result.execution_gas_spent();
416 self.state_gas_used += result.state_gas_spent();
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::EthApiBuilder;
424 use alloy_consensus::{transaction::SignerRecoverable, TxEip1559};
425 use alloy_eips::{eip1559::INITIAL_BASE_FEE, eip2718::Encodable2718, BlockNumberOrTag};
426 use alloy_genesis::{Genesis, GenesisAccount};
427 use alloy_primitives::{Address, TxKind};
428 use reth_chain_state::ExecutedBlock;
429 use reth_chainspec::ChainSpecBuilder;
430 use reth_db_common::init::init_genesis;
431 use reth_ethereum_primitives::Block as EthBlock;
432 use reth_evm_ethereum::EthEvmConfig;
433 use reth_network_api::noop::NoopNetwork;
434 use reth_primitives_traits::RecoveredBlock;
435 use reth_provider::{
436 providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
437 };
438 use reth_rpc_eth_api::helpers::SpawnBlocking;
439 use reth_storage_overlay::OverlayManager;
440 use reth_testing_utils::generators::{self, generate_key, sign_tx_with_key_pair};
441 use reth_transaction_pool::test_utils::testing_pool;
442
443 #[tokio::test(flavor = "multi_thread")]
446 async fn call_bundle_uses_next_block_base_fee() {
447 let mut rng = generators::rng();
448 let key = generate_key(&mut rng);
449 let next_base_fee = INITIAL_BASE_FEE - INITIAL_BASE_FEE / 8;
451 let tx = sign_tx_with_key_pair(
452 key,
453 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
454 chain_id: 1,
455 nonce: 0,
456 gas_limit: 21_000,
457 max_fee_per_gas: (next_base_fee + 1) as u128,
458 max_priority_fee_per_gas: (next_base_fee + 1) as u128,
459 to: TxKind::Call(Address::random()),
460 ..Default::default()
461 }),
462 );
463 let sender = tx.recover_signer().unwrap();
464
465 let genesis = Genesis::default()
466 .with_gas_limit(30_000_000)
467 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
468 .extend_accounts([(
469 sender,
470 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
471 )]);
472 let chain_spec =
473 Arc::new(ChainSpecBuilder::mainnet().cancun_activated().genesis(genesis).build());
474 let overlay_manager = OverlayManager::default();
475 let factory = create_test_provider_factory_with_chain_spec(chain_spec)
476 .with_overlay_manager(overlay_manager.clone());
477 init_genesis(&factory).unwrap();
478 let provider = BlockchainProvider::new(factory).unwrap();
479
480 let eth_api = EthApiBuilder::new(
481 provider.clone(),
482 testing_pool(),
483 NoopNetwork::default(),
484 EthEvmConfig::new(provider.chain_spec()),
485 )
486 .build();
487 let api = EthBundle::new(eth_api, BlockingTaskGuard::new(4));
488
489 let bundle = EthCallBundle {
490 txs: vec![tx.encoded_2718().into()],
491 block_number: 1,
492 state_block_number: BlockNumberOrTag::Number(0),
493 ..Default::default()
494 };
495 for state_block_number in
496 [BlockNumberOrTag::Number(0), BlockNumberOrTag::Latest, BlockNumberOrTag::Pending]
497 {
498 for base_fee in [None, Some((next_base_fee - 1) as u128)] {
499 let response = api
500 .call_bundle(EthCallBundle { state_block_number, base_fee, ..bundle.clone() })
501 .await
502 .unwrap();
503 let expected_tip =
504 (next_base_fee + 1) as u128 - base_fee.unwrap_or(next_base_fee as u128);
505 assert_bundle_fees(&response, expected_tip);
506 }
507 }
508
509 let error = api
510 .call_bundle(EthCallBundle { base_fee: Some(u128::MAX), ..bundle.clone() })
511 .await
512 .unwrap_err();
513 assert!(
514 matches!(
515 error.as_invalid_transaction(),
516 Some(reth_rpc_eth_types::RpcInvalidTransactionError::FeeCapTooLow)
517 ),
518 "{error:?}"
519 );
520
521 let chain_spec = provider.chain_spec();
523 let mut header = chain_spec.genesis_header().clone();
524 header.parent_hash = chain_spec.genesis_hash();
525 header.number = 1;
526 header.timestamp = 12;
527 header.base_fee_per_gas = Some(next_base_fee);
528 let pending_block = ExecutedBlock {
529 recovered_block: Arc::new(RecoveredBlock::new_unhashed(
530 EthBlock { header, body: Default::default() },
531 vec![],
532 )),
533 ..Default::default()
534 };
535 overlay_manager.insert_block(pending_block.clone());
536 provider.canonical_in_memory_state().set_pending_block(pending_block);
537 let pending_next_base_fee = next_base_fee - next_base_fee / 8;
538 for base_fee in [None, Some((pending_next_base_fee - 1) as u128)] {
539 let response = api
540 .call_bundle(EthCallBundle {
541 block_number: 2,
542 state_block_number: BlockNumberOrTag::Pending,
543 base_fee,
544 ..bundle.clone()
545 })
546 .await
547 .unwrap();
548 let expected_tip =
549 (next_base_fee + 1) as u128 - base_fee.unwrap_or(pending_next_base_fee as u128);
550 assert_bundle_fees(&response, expected_tip);
551 }
552 }
553
554 #[tokio::test(flavor = "multi_thread")]
557 async fn call_bundle_shares_gas_limit_and_takes_blocking_io_permit() {
558 let mut rng = generators::rng();
559 let key = generate_key(&mut rng);
560 let txs: Vec<_> = (0..2)
561 .map(|nonce| {
562 sign_tx_with_key_pair(
563 key,
564 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
565 chain_id: 1,
566 nonce,
567 gas_limit: 21_000,
568 max_fee_per_gas: INITIAL_BASE_FEE as u128,
569 to: TxKind::Call(Address::random()),
570 ..Default::default()
571 }),
572 )
573 })
574 .collect();
575 let sender = txs[0].recover_signer().unwrap();
576
577 let genesis = Genesis::default()
578 .with_gas_limit(30_000_000)
579 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
580 .extend_accounts([(
581 sender,
582 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
583 )]);
584 let chain_spec =
585 Arc::new(ChainSpecBuilder::mainnet().cancun_activated().genesis(genesis).build());
586 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
587 init_genesis(&factory).unwrap();
588 let provider = BlockchainProvider::new(factory).unwrap();
589
590 let eth_api = EthApiBuilder::new(
591 provider.clone(),
592 testing_pool(),
593 NoopNetwork::default(),
594 EthEvmConfig::new(provider.chain_spec()),
595 )
596 .max_blocking_io_requests(1)
597 .build();
598 let api = EthBundle::new(eth_api.clone(), BlockingTaskGuard::new(4));
599
600 let bundle = EthCallBundle {
601 txs: txs.iter().map(|tx| tx.encoded_2718().into()).collect(),
602 block_number: 1,
603 state_block_number: BlockNumberOrTag::Number(0),
604 ..Default::default()
605 };
606
607 let error = api
608 .call_bundle(EthCallBundle { gas_limit: Some(41_999), ..bundle.clone() })
609 .await
610 .unwrap_err();
611 assert_eq!(
612 error.to_string(),
613 EthApiError::InvalidParams(EthBundleError::BundleGasLimitExceeded(41_999).to_string())
614 .to_string()
615 );
616
617 let bundle = EthCallBundle { gas_limit: Some(42_000), ..bundle };
618 let permit = eth_api.acquire_owned_blocking_io().await.unwrap();
619 assert!(tokio::time::timeout(
620 std::time::Duration::from_millis(200),
621 api.call_bundle(bundle.clone())
622 )
623 .await
624 .is_err());
625
626 drop(permit);
627 let response = api.call_bundle(bundle).await.unwrap();
628 assert_eq!(response.total_gas_used, 42_000);
629 }
630
631 #[tokio::test(flavor = "multi_thread")]
635 async fn call_bundle_uses_separate_gas_pools_after_amsterdam() {
636 const TX_GAS_LIMIT: u64 = 1_000_000;
637
638 let mut rng = generators::rng();
639 let key = generate_key(&mut rng);
640 let txs: Vec<_> = (0..2)
642 .map(|nonce| {
643 sign_tx_with_key_pair(
644 key,
645 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
646 chain_id: 1,
647 nonce,
648 gas_limit: TX_GAS_LIMIT,
649 max_fee_per_gas: INITIAL_BASE_FEE as u128,
650 to: TxKind::Call(Address::random()),
651 value: U256::from(1),
652 ..Default::default()
653 }),
654 )
655 })
656 .collect();
657 let sender = txs[0].recover_signer().unwrap();
658
659 let genesis = Genesis::default()
660 .with_gas_limit(30_000_000)
661 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
662 .extend_accounts([(
663 sender,
664 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
665 )]);
666 let chain_spec =
667 Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().genesis(genesis).build());
668 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
669 init_genesis(&factory).unwrap();
670 let provider = BlockchainProvider::new(factory).unwrap();
671
672 let eth_api = EthApiBuilder::new(
673 provider.clone(),
674 testing_pool(),
675 NoopNetwork::default(),
676 EthEvmConfig::new(provider.chain_spec()),
677 )
678 .build();
679 let api = EthBundle::new(eth_api, BlockingTaskGuard::new(4));
680
681 let bundle = EthCallBundle {
682 txs: txs.iter().map(|tx| tx.encoded_2718().into()).collect(),
683 block_number: 1,
684 state_block_number: BlockNumberOrTag::Number(0),
685 ..Default::default()
686 };
687 let first_gas_used = api.call_bundle(bundle.clone()).await.unwrap().results[0].gas_used;
688
689 let gas_limit = TX_GAS_LIMIT + first_gas_used - 1;
692 let response =
693 api.call_bundle(EthCallBundle { gas_limit: Some(gas_limit), ..bundle }).await.unwrap();
694 assert_eq!(response.results.len(), 2);
695 }
696
697 #[tokio::test(flavor = "multi_thread")]
700 async fn call_bundle_rejects_fee_cap_below_base_fee_before_london() {
701 let mut rng = generators::rng();
702 let key = generate_key(&mut rng);
703 let tx = sign_tx_with_key_pair(
704 key,
705 reth_ethereum_primitives::Transaction::Legacy(alloy_consensus::TxLegacy {
706 chain_id: Some(1),
707 nonce: 0,
708 gas_price: 1,
709 gas_limit: 21_000,
710 to: TxKind::Call(Address::random()),
711 ..Default::default()
712 }),
713 );
714 let sender = tx.recover_signer().unwrap();
715
716 let genesis = Genesis::default().with_gas_limit(30_000_000).extend_accounts([(
717 sender,
718 GenesisAccount::default().with_balance(U256::from(10u128.pow(18))),
719 )]);
720 let chain_spec =
721 Arc::new(ChainSpecBuilder::mainnet().berlin_activated().genesis(genesis).build());
722 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
723 init_genesis(&factory).unwrap();
724 let provider = BlockchainProvider::new(factory).unwrap();
725
726 let eth_api = EthApiBuilder::new(
727 provider.clone(),
728 testing_pool(),
729 NoopNetwork::default(),
730 EthEvmConfig::new(provider.chain_spec()),
731 )
732 .build();
733 let api = EthBundle::new(eth_api, BlockingTaskGuard::new(4));
734
735 let error = api
736 .call_bundle(EthCallBundle {
737 txs: vec![tx.encoded_2718().into()],
738 block_number: 1,
739 state_block_number: BlockNumberOrTag::Number(0),
740 base_fee: Some(100),
741 ..Default::default()
742 })
743 .await
744 .unwrap_err();
745 assert!(
746 matches!(
747 error.as_invalid_transaction(),
748 Some(RpcInvalidTransactionError::FeeCapTooLow)
749 ),
750 "{error:?}"
751 );
752 }
753
754 #[tokio::test(flavor = "multi_thread")]
757 async fn call_bundle_counts_fees_after_coinbase_selfdestruct() {
758 let mut rng = generators::rng();
759 let key = generate_key(&mut rng);
760 let tip = INITIAL_BASE_FEE - INITIAL_BASE_FEE / 8 + 1;
761 let coinbase = Address::random();
762 let tx = |nonce, to| {
763 sign_tx_with_key_pair(
764 key,
765 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
766 chain_id: 1,
767 nonce,
768 gas_limit: 100_000,
769 max_fee_per_gas: tip as u128 * 2,
770 max_priority_fee_per_gas: tip as u128,
771 to: TxKind::Call(to),
772 ..Default::default()
773 }),
774 )
775 };
776 let txs = [tx(0, coinbase), tx(1, Address::random())];
777 let sender = txs[0].recover_signer().unwrap();
778
779 let selfdestruct_code = [0x5f, 0xff];
781 let genesis = Genesis::default()
782 .with_gas_limit(30_000_000)
783 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
784 .extend_accounts([
785 (sender, GenesisAccount::default().with_balance(U256::from(10u128.pow(18)))),
786 (
787 coinbase,
788 GenesisAccount::default()
789 .with_balance(U256::from(10u128.pow(18)))
790 .with_code(Some(selfdestruct_code.into())),
791 ),
792 ]);
793 let chain_spec =
794 Arc::new(ChainSpecBuilder::mainnet().shanghai_activated().genesis(genesis).build());
795 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
796 init_genesis(&factory).unwrap();
797 let provider = BlockchainProvider::new(factory).unwrap();
798
799 let eth_api = EthApiBuilder::new(
800 provider.clone(),
801 testing_pool(),
802 NoopNetwork::default(),
803 EthEvmConfig::new(provider.chain_spec()),
804 )
805 .build();
806 let api = EthBundle::new(eth_api, BlockingTaskGuard::new(4));
807
808 let response = api
809 .call_bundle(EthCallBundle {
810 txs: txs.iter().map(|tx| tx.encoded_2718().into()).collect(),
811 block_number: 1,
812 state_block_number: BlockNumberOrTag::Number(0),
813 coinbase: Some(coinbase),
814 ..Default::default()
815 })
816 .await
817 .unwrap();
818
819 assert_eq!(response.results[0].revert, None);
820 assert_eq!(response.results[1].coinbase_diff, response.results[1].gas_fees);
821 }
822
823 #[tokio::test(flavor = "multi_thread")]
826 async fn call_bundle_times_out() {
827 let mut rng = generators::rng();
828 let key = generate_key(&mut rng);
829 let looping = Address::random();
830 let tx = |nonce, to, gas_limit| {
831 sign_tx_with_key_pair(
832 key,
833 reth_ethereum_primitives::Transaction::Eip1559(TxEip1559 {
834 chain_id: 1,
835 nonce,
836 gas_limit,
837 max_fee_per_gas: INITIAL_BASE_FEE as u128,
838 to: TxKind::Call(to),
839 ..Default::default()
840 }),
841 )
842 };
843 let slow = tx(0, looping, 10_000_000);
845 let transfer = |nonce| tx(nonce, Address::random(), 21_000);
846 let sender = slow.recover_signer().unwrap();
847
848 let looping_code = [0x5b, 0x5f, 0x56];
850 let genesis = Genesis::default()
851 .with_gas_limit(30_000_000)
852 .with_base_fee(Some(INITIAL_BASE_FEE as u128))
853 .extend_accounts([
854 (sender, GenesisAccount::default().with_balance(U256::from(10u128.pow(18)))),
855 (looping, GenesisAccount::default().with_code(Some(looping_code.into()))),
856 ]);
857 let chain_spec =
858 Arc::new(ChainSpecBuilder::mainnet().cancun_activated().genesis(genesis).build());
859 let factory = create_test_provider_factory_with_chain_spec(chain_spec);
860 init_genesis(&factory).unwrap();
861 let provider = BlockchainProvider::new(factory).unwrap();
862
863 let eth_api = EthApiBuilder::new(
864 provider.clone(),
865 testing_pool(),
866 NoopNetwork::default(),
867 EthEvmConfig::new(provider.chain_spec()),
868 )
869 .build();
870 let api = EthBundle::new(eth_api, BlockingTaskGuard::new(4));
871
872 let bundle = EthCallBundle {
873 block_number: 1,
874 state_block_number: BlockNumberOrTag::Number(0),
875 ..Default::default()
876 };
877
878 let error = api
879 .call_bundle(EthCallBundle {
880 txs: vec![slow.encoded_2718().into(), transfer(1).encoded_2718().into()],
881 timeout: Some(1),
882 ..bundle.clone()
883 })
884 .await
885 .unwrap_err();
886 assert!(
887 matches!(
888 error,
889 EthApiError::ExecutionTimedOut(timeout) if timeout == Duration::from_millis(1)
890 ),
891 "{error:?}"
892 );
893
894 let response = api
895 .call_bundle(EthCallBundle {
896 txs: vec![transfer(0).encoded_2718().into()],
897 timeout: Some(0),
898 ..bundle
899 })
900 .await
901 .unwrap();
902 assert_eq!(response.results.len(), 1);
903 }
904
905 fn assert_bundle_fees(response: &EthCallBundleResponse, expected_tip: u128) {
906 assert_eq!(response.results.len(), 1);
907 assert_eq!(response.total_gas_used, 21_000);
908 let gas_price = U256::from(expected_tip);
909 let gas_fees = gas_price * U256::from(21_000);
910 assert_eq!(response.results[0].gas_price, gas_price);
911 assert_eq!(response.results[0].gas_fees, gas_fees);
912 assert_eq!(response.gas_fees, gas_fees);
913 assert_eq!(response.bundle_gas_price, gas_price);
914 assert_eq!(response.coinbase_diff, gas_fees);
915 }
916}