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