reth_rpc_eth_api/helpers/
pending_block.rs

1//! Loads a pending block from database. Helper trait for `eth_` block, transaction, call and trace
2//! RPC methods.
3
4use super::SpawnBlocking;
5use crate::{types::RpcTypes, EthApiTypes, FromEthApiError, FromEvmError, RpcNodeCore};
6use alloy_consensus::{BlockHeader, Transaction};
7use alloy_eips::eip4844::MAX_DATA_GAS_PER_BLOCK;
8use alloy_rpc_types_eth::BlockNumberOrTag;
9use futures::Future;
10use reth_chainspec::{EthChainSpec, EthereumHardforks};
11use reth_errors::{BlockExecutionError, BlockValidationError, RethError};
12use reth_evm::{
13    execute::{BlockBuilder, BlockBuilderOutcome},
14    ConfigureEvm, Evm, SpecFor,
15};
16use reth_node_api::NodePrimitives;
17use reth_primitives_traits::{
18    transaction::error::InvalidTransactionError, Receipt, RecoveredBlock, SealedHeader,
19};
20use reth_provider::{
21    BlockReader, BlockReaderIdExt, ChainSpecProvider, ProviderBlock, ProviderError, ProviderHeader,
22    ProviderReceipt, ProviderTx, ReceiptProvider, StateProviderFactory,
23};
24use reth_revm::{database::StateProviderDatabase, db::State};
25use reth_rpc_eth_types::{EthApiError, PendingBlock, PendingBlockEnv, PendingBlockEnvOrigin};
26use reth_transaction_pool::{
27    error::InvalidPoolTransactionError, BestTransactionsAttributes, PoolTransaction,
28    TransactionPool,
29};
30use revm::context_interface::Block;
31use std::time::{Duration, Instant};
32use tokio::sync::Mutex;
33use tracing::debug;
34
35/// Loads a pending block from database.
36///
37/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` blocks RPC methods.
38pub trait LoadPendingBlock:
39    EthApiTypes<
40        NetworkTypes: RpcTypes<
41            Header = alloy_rpc_types_eth::Header<ProviderHeader<Self::Provider>>,
42        >,
43        Error: FromEvmError<Self::Evm>,
44    > + RpcNodeCore<
45        Provider: BlockReaderIdExt<Receipt: Receipt>
46                      + ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks>
47                      + StateProviderFactory,
48        Pool: TransactionPool<Transaction: PoolTransaction<Consensus = ProviderTx<Self::Provider>>>,
49        Evm: ConfigureEvm<
50            Primitives: NodePrimitives<
51                BlockHeader = ProviderHeader<Self::Provider>,
52                SignedTx = ProviderTx<Self::Provider>,
53                Receipt = ProviderReceipt<Self::Provider>,
54                Block = ProviderBlock<Self::Provider>,
55            >,
56        >,
57    >
58{
59    /// Returns a handle to the pending block.
60    ///
61    /// Data access in default (L1) trait method implementations.
62    #[expect(clippy::type_complexity)]
63    fn pending_block(
64        &self,
65    ) -> &Mutex<Option<PendingBlock<ProviderBlock<Self::Provider>, ProviderReceipt<Self::Provider>>>>;
66
67    /// Configures the [`PendingBlockEnv`] for the pending block
68    ///
69    /// If no pending block is available, this will derive it from the `latest` block
70    #[expect(clippy::type_complexity)]
71    fn pending_block_env_and_cfg(
72        &self,
73    ) -> Result<
74        PendingBlockEnv<
75            ProviderBlock<Self::Provider>,
76            ProviderReceipt<Self::Provider>,
77            SpecFor<Self::Evm>,
78        >,
79        Self::Error,
80    > {
81        if let Some(block) =
82            self.provider().pending_block_with_senders().map_err(Self::Error::from_eth_err)?
83        {
84            if let Some(receipts) = self
85                .provider()
86                .receipts_by_block(block.hash().into())
87                .map_err(Self::Error::from_eth_err)?
88            {
89                // Note: for the PENDING block we assume it is past the known merge block and
90                // thus this will not fail when looking up the total
91                // difficulty value for the blockenv.
92                let evm_env = self.evm_config().evm_env(block.header());
93
94                return Ok(PendingBlockEnv::new(
95                    evm_env,
96                    PendingBlockEnvOrigin::ActualPending(block, receipts),
97                ));
98            }
99        }
100
101        // no pending block from the CL yet, so we use the latest block and modify the env
102        // values that we can
103        let latest = self
104            .provider()
105            .latest_header()
106            .map_err(Self::Error::from_eth_err)?
107            .ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()))?;
108
109        let evm_env = self
110            .evm_config()
111            .next_evm_env(&latest, &self.next_env_attributes(&latest)?)
112            .map_err(RethError::other)
113            .map_err(Self::Error::from_eth_err)?;
114
115        Ok(PendingBlockEnv::new(evm_env, PendingBlockEnvOrigin::DerivedFromLatest(latest)))
116    }
117
118    /// Returns [`ConfigureEvm::NextBlockEnvCtx`] for building a local pending block.
119    fn next_env_attributes(
120        &self,
121        parent: &SealedHeader<ProviderHeader<Self::Provider>>,
122    ) -> Result<<Self::Evm as ConfigureEvm>::NextBlockEnvCtx, Self::Error>;
123
124    /// Returns the locally built pending block
125    #[expect(clippy::type_complexity)]
126    fn local_pending_block(
127        &self,
128    ) -> impl Future<
129        Output = Result<
130            Option<(
131                RecoveredBlock<<Self::Provider as BlockReader>::Block>,
132                Vec<ProviderReceipt<Self::Provider>>,
133            )>,
134            Self::Error,
135        >,
136    > + Send
137    where
138        Self: SpawnBlocking,
139    {
140        async move {
141            let pending = self.pending_block_env_and_cfg()?;
142            let parent = match pending.origin {
143                PendingBlockEnvOrigin::ActualPending(block, receipts) => {
144                    return Ok(Some((block, receipts)));
145                }
146                PendingBlockEnvOrigin::DerivedFromLatest(parent) => parent,
147            };
148
149            // we couldn't find the real pending block, so we need to build it ourselves
150            let mut lock = self.pending_block().lock().await;
151
152            let now = Instant::now();
153
154            // check if the block is still good
155            if let Some(pending_block) = lock.as_ref() {
156                // this is guaranteed to be the `latest` header
157                if pending.evm_env.block_env.number == pending_block.block.number() &&
158                    parent.hash() == pending_block.block.parent_hash() &&
159                    now <= pending_block.expires_at
160                {
161                    return Ok(Some((pending_block.block.clone(), pending_block.receipts.clone())));
162                }
163            }
164
165            // no pending block from the CL yet, so we need to build it ourselves via txpool
166            let (sealed_block, receipts) = match self
167                .spawn_blocking_io(move |this| {
168                    // we rebuild the block
169                    this.build_block(&parent)
170                })
171                .await
172            {
173                Ok(block) => block,
174                Err(err) => {
175                    debug!(target: "rpc", "Failed to build pending block: {:?}", err);
176                    return Ok(None)
177                }
178            };
179
180            let now = Instant::now();
181            *lock = Some(PendingBlock::new(
182                now + Duration::from_secs(1),
183                sealed_block.clone(),
184                receipts.clone(),
185            ));
186
187            Ok(Some((sealed_block, receipts)))
188        }
189    }
190
191    /// Builds a pending block using the configured provider and pool.
192    ///
193    /// If the origin is the actual pending block, the block is built with withdrawals.
194    ///
195    /// After Cancun, if the origin is the actual pending block, the block includes the EIP-4788 pre
196    /// block contract call using the parent beacon block root received from the CL.
197    #[expect(clippy::type_complexity)]
198    fn build_block(
199        &self,
200        parent: &SealedHeader<ProviderHeader<Self::Provider>>,
201    ) -> Result<
202        (RecoveredBlock<ProviderBlock<Self::Provider>>, Vec<ProviderReceipt<Self::Provider>>),
203        Self::Error,
204    >
205    where
206        EthApiError: From<ProviderError>,
207    {
208        let state_provider = self
209            .provider()
210            .history_by_block_hash(parent.hash())
211            .map_err(Self::Error::from_eth_err)?;
212        let state = StateProviderDatabase::new(&state_provider);
213        let mut db = State::builder().with_database(state).with_bundle_update().build();
214
215        let mut builder = self
216            .evm_config()
217            .builder_for_next_block(&mut db, parent, self.next_env_attributes(parent)?)
218            .map_err(RethError::other)
219            .map_err(Self::Error::from_eth_err)?;
220
221        builder.apply_pre_execution_changes().map_err(Self::Error::from_eth_err)?;
222
223        let block_env = builder.evm_mut().block().clone();
224
225        let mut cumulative_gas_used = 0;
226        let mut sum_blob_gas_used = 0;
227        let block_gas_limit: u64 = block_env.gas_limit;
228
229        let mut best_txs =
230            self.pool().best_transactions_with_attributes(BestTransactionsAttributes::new(
231                block_env.basefee,
232                block_env.blob_gasprice().map(|gasprice| gasprice as u64),
233            ));
234
235        while let Some(pool_tx) = best_txs.next() {
236            // ensure we still have capacity for this transaction
237            if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
238                // we can't fit this transaction into the block, so we need to mark it as invalid
239                // which also removes all dependent transaction from the iterator before we can
240                // continue
241                best_txs.mark_invalid(
242                    &pool_tx,
243                    InvalidPoolTransactionError::ExceedsGasLimit(
244                        pool_tx.gas_limit(),
245                        block_gas_limit,
246                    ),
247                );
248                continue
249            }
250
251            if pool_tx.origin.is_private() {
252                // we don't want to leak any state changes made by private transactions, so we mark
253                // them as invalid here which removes all dependent transactions from the iterator
254                // before we can continue
255                best_txs.mark_invalid(
256                    &pool_tx,
257                    InvalidPoolTransactionError::Consensus(
258                        InvalidTransactionError::TxTypeNotSupported,
259                    ),
260                );
261                continue
262            }
263
264            // convert tx to a signed transaction
265            let tx = pool_tx.to_consensus();
266
267            // There's only limited amount of blob space available per block, so we need to check if
268            // the EIP-4844 can still fit in the block
269            if let Some(tx_blob_gas) = tx.blob_gas_used() {
270                if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK {
271                    // we can't fit this _blob_ transaction into the block, so we mark it as
272                    // invalid, which removes its dependent transactions from
273                    // the iterator. This is similar to the gas limit condition
274                    // for regular transactions above.
275                    best_txs.mark_invalid(
276                        &pool_tx,
277                        InvalidPoolTransactionError::ExceedsGasLimit(
278                            tx_blob_gas,
279                            MAX_DATA_GAS_PER_BLOCK,
280                        ),
281                    );
282                    continue
283                }
284            }
285
286            let gas_used = match builder.execute_transaction(tx.clone()) {
287                Ok(gas_used) => gas_used,
288                Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
289                    error,
290                    ..
291                })) => {
292                    if error.is_nonce_too_low() {
293                        // if the nonce is too low, we can skip this transaction
294                    } else {
295                        // if the transaction is invalid, we can skip it and all of its
296                        // descendants
297                        best_txs.mark_invalid(
298                            &pool_tx,
299                            InvalidPoolTransactionError::Consensus(
300                                InvalidTransactionError::TxTypeNotSupported,
301                            ),
302                        );
303                    }
304                    continue
305                }
306                // this is an error that we should treat as fatal for this attempt
307                Err(err) => return Err(Self::Error::from_eth_err(err)),
308            };
309
310            // add to the total blob gas used if the transaction successfully executed
311            if let Some(tx_blob_gas) = tx.blob_gas_used() {
312                sum_blob_gas_used += tx_blob_gas;
313
314                // if we've reached the max data gas per block, we can skip blob txs entirely
315                if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK {
316                    best_txs.skip_blobs();
317                }
318            }
319
320            // add gas used by the transaction to cumulative gas used, before creating the receipt
321            cumulative_gas_used += gas_used;
322        }
323
324        let BlockBuilderOutcome { execution_result, block, .. } =
325            builder.finish(&state_provider).map_err(Self::Error::from_eth_err)?;
326
327        Ok((block, execution_result.receipts))
328    }
329}