Skip to main content

reth_rpc/eth/
bundle.rs

1//! `Eth` bundle implementation and helpers.
2
3use 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
29/// `Eth` bundle implementation.
30pub struct EthBundle<Eth> {
31    /// All nested fields bundled together.
32    inner: Arc<EthBundleInner<Eth>>,
33}
34
35impl<Eth> EthBundle<Eth> {
36    /// Create a new `EthBundle` instance.
37    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    /// Access the underlying `Eth` API.
42    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    /// Simulates a bundle of transactions at the top of a given block number with the state of
52    /// another (or the same) block. This can be used to simulate future blocks with the current
53    /// state, or it can be used to simulate a past block. The sender is responsible for signing the
54    /// transactions and using the correct nonce and ensuring validity
55    ///
56    /// The simulation is bounded by the request's `timeout` in milliseconds, capped at 30
57    /// seconds, or by 5 seconds if it is missing or zero. No transaction starts after it expires,
58    /// the request then fails with [`EthApiError::ExecutionTimedOut`].
59    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        // Validate gas limit against the configured call gas limit before any DB calls
89        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        // A zero timeout selects the default instead of expiring immediately.
99        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        // Note: the block number is considered the `parent` block: <https://github.com/flashbots/mev-geth/blob/fddf97beec5877483f879a77b7dea2e58a58d653/internal/ethapi/api.go#L2104>
109        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        // need to adjust the timestamp for the next block
133        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        // Validate that the bundle does not contain more than MAX_BLOB_NUMBER_PER_BLOCK blob
144        // transactions.
145        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        // Apply gas limit: default to call gas limit unless user requests a smaller limit
163        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        // The bundle is simulated on top of the state block, so default to the next block's base
167        // fee. <https://github.com/flashbots/mev-geth/blob/fddf97beec5877483f879a77b7dea2e58a58d653/internal/ethapi/api.go#L2130>
168        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        // use the block number of the request
181        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                // Starts here, so waiting for the permit does not count.
209                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                    // Fee caps below the base fee are only rejected by the EVM from London on.
246                    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                    // update the coinbase balance
271                    coinbase_balance_before_tx = coinbase_balance_after_tx;
272
273                    // set the return data for the response
274                    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                // populate the response
298
299                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/// Container type for `EthBundle` internals
331#[derive(Debug)]
332struct EthBundleInner<Eth> {
333    /// Access to commonly used code of the `eth` namespace
334    eth_api: Eth,
335    // restrict the number of concurrent tracing calls.
336    #[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/// [`EthBundle`] specific errors.
353#[derive(Debug, thiserror::Error)]
354pub enum EthBundleError {
355    /// Thrown if the bundle does not contain any transactions.
356    #[error("bundle missing txs")]
357    EmptyBundleTransactions,
358    /// Thrown if the bundle does not contain a block number, or block number is 0.
359    #[error("bundle missing blockNumber")]
360    BundleMissingBlockNumber,
361    /// Thrown when the blob gas usage of the blob transactions in a bundle exceed the maximum.
362    #[error("blob gas usage exceeds the limit of {0} gas per block.")]
363    Eip4844BlobGasExceeded(u64),
364    /// Thrown when the gas limits of the bundle's transactions exceed the block gas limit.
365    #[error("bundle gas exceeds the block gas limit of {0}")]
366    BundleGasLimitExceeded(u64),
367}
368
369/// Gas a simulated bundle uses from the block it is simulated in.
370///
371/// Transactions are admitted against the gas left in the block, like the block executor does:
372/// EIP-8037 keeps separate regular and state gas pools.
373#[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    /// Creates an unused budget of `limit` gas for a block executed with `version`.
385    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    /// Returns the receipt gas used by the recorded transactions.
397    pub(crate) const fn gas_used(&self) -> u64 {
398        self.gas_used
399    }
400
401    /// Returns whether a transaction with `gas_limit` fits in the gas left in the block.
402    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    /// Records the gas spent by an executed transaction.
413    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    /// The bundle is simulated in the block after the state block, so a transaction that only
444    /// covers the next block's base fee, but not the state block's, must be accepted.
445    #[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        // genesis is empty, so the next base fee is 7/8 of the initial base fee
450        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        // An actual pending block is not canonical and has its own base fee.
522        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    /// The bundle's transactions share the block gas limit, and the simulation waits for a
555    /// blocking IO permit.
556    #[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    /// Under EIP-8037 the bundle admits transactions against separate regular and state gas
632    /// pools, so a bundle that fits a block is accepted even when its receipt gas does not fit
633    /// one pool.
634    #[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        // Value transfers to new accounts spend both regular and state gas.
641        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        // The second transaction fits both pools, but not one pool holding the first
690        // transaction's regular and state gas together.
691        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    /// A pre-London state has no base fee check in the EVM, so the bundle must reject a fee cap
698    /// below the supplied base fee instead of panicking.
699    #[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    /// A pre-Cancun transaction can selfdestruct the coinbase; later fees must then be counted
755    /// from a zero balance.
756    #[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        // `PUSH0 SELFDESTRUCT`
780        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    /// No transaction starts once the bundle's timeout has expired, and a zero timeout selects
824    /// the default instead of expiring immediately.
825    #[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        // Burns its whole gas limit, which takes longer than 1ms.
844        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        // `JUMPDEST PUSH0 JUMP`
849        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}