Skip to main content

reth_ethereum_payload_builder/
lib.rs

1//! A basic Ethereum payload builder implementation.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11use alloy_consensus::Transaction;
12use alloy_primitives::U256;
13use alloy_rlp::Encodable;
14use alloy_rpc_types_engine::PayloadAttributes as EthPayloadAttributes;
15use reth_basic_payload_builder::{
16    is_better_payload, BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder,
17    PayloadConfig,
18};
19use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
20use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
21use reth_errors::{BlockExecutionError, BlockValidationError, ConsensusError};
22use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
23use reth_evm::{
24    execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
25    ConfigureEvm, Evm, NextBlockEnvAttributes,
26};
27use reth_evm_ethereum::EthEvmConfig;
28use reth_execution_cache::CachedStateProvider;
29use reth_payload_builder::{BlobSidecars, EthBuiltPayload};
30use reth_payload_builder_primitives::PayloadBuilderError;
31use reth_payload_primitives::PayloadAttributes;
32use reth_primitives_traits::transaction::error::InvalidTransactionError;
33use reth_revm::{database::StateProviderDatabase, db::State};
34use reth_storage_api::StateProviderFactory;
35use reth_transaction_pool::{
36    error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
37    BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
38    ValidPoolTransaction,
39};
40use revm::context_interface::Block as _;
41use std::sync::Arc;
42use tracing::{debug, trace, warn};
43
44mod config;
45pub use config::*;
46
47pub mod validator;
48pub use validator::EthereumExecutionPayloadValidator;
49
50type BestTransactionsIter<Pool> = Box<
51    dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
52>;
53
54/// Ethereum payload builder
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
57    /// Client providing access to node state.
58    client: Client,
59    /// Transaction pool.
60    pool: Pool,
61    /// The type responsible for creating the evm.
62    evm_config: EvmConfig,
63    /// Payload builder configuration.
64    builder_config: EthereumBuilderConfig,
65}
66
67impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
68    /// `EthereumPayloadBuilder` constructor.
69    pub const fn new(
70        client: Client,
71        pool: Pool,
72        evm_config: EvmConfig,
73        builder_config: EthereumBuilderConfig,
74    ) -> Self {
75        Self { client, pool, evm_config, builder_config }
76    }
77}
78
79// Default implementation of [PayloadBuilder] for unit type
80impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
81where
82    EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
83    Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
84    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
85{
86    type Attributes = EthPayloadAttributes;
87    type BuiltPayload = EthBuiltPayload;
88
89    fn try_build(
90        &self,
91        args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
92    ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
93        default_ethereum_payload(
94            self.evm_config.clone(),
95            self.client.clone(),
96            self.pool.clone(),
97            self.builder_config.clone(),
98            args,
99            |attributes| self.pool.best_transactions_with_attributes(attributes),
100        )
101    }
102
103    fn on_missing_payload(
104        &self,
105        _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
106    ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
107        if self.builder_config.await_payload_on_missing {
108            MissingPayloadBehaviour::AwaitInProgress
109        } else {
110            MissingPayloadBehaviour::RaceEmptyPayload
111        }
112    }
113
114    fn build_empty_payload(
115        &self,
116        config: PayloadConfig<Self::Attributes>,
117    ) -> Result<EthBuiltPayload, PayloadBuilderError> {
118        let args = BuildArguments::new(
119            Default::default(),
120            Default::default(),
121            None,
122            config,
123            Default::default(),
124            None,
125        );
126
127        default_ethereum_payload(
128            self.evm_config.clone(),
129            self.client.clone(),
130            self.pool.clone(),
131            self.builder_config.clone(),
132            args,
133            |attributes| self.pool.best_transactions_with_attributes(attributes),
134        )?
135        .into_payload()
136        .ok_or_else(|| PayloadBuilderError::MissingPayload)
137    }
138}
139
140/// Constructs an Ethereum transaction payload using the best transactions from the pool.
141///
142/// Given build arguments including an Ethereum client, transaction pool,
143/// and configuration, this function creates a transaction payload. Returns
144/// a result indicating success with the payload or an error in case of failure.
145#[inline]
146pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
147    evm_config: EvmConfig,
148    client: Client,
149    pool: Pool,
150    builder_config: EthereumBuilderConfig,
151    args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
152    best_txs: F,
153) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
154where
155    EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
156    Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
157    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
158    F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
159{
160    let BuildArguments {
161        mut cached_reads,
162        execution_cache,
163        trie_handle,
164        config,
165        cancel,
166        best_payload,
167    } = args;
168    let PayloadConfig { parent_header, attributes, payload_id } = config;
169
170    let mut state_provider = client.state_by_block_hash(parent_header.hash())?;
171    if let Some(execution_cache) = execution_cache {
172        state_provider = Box::new(CachedStateProvider::new(
173            state_provider,
174            execution_cache.cache().clone(),
175            execution_cache.metrics().clone(),
176        ));
177    }
178    let state = StateProviderDatabase::new(state_provider.as_ref());
179    let mut db =
180        State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
181
182    let mut builder = evm_config
183        .builder_for_next_block(
184            &mut db,
185            &parent_header,
186            NextBlockEnvAttributes {
187                timestamp: attributes.timestamp(),
188                suggested_fee_recipient: attributes.suggested_fee_recipient,
189                prev_randao: attributes.prev_randao,
190                gas_limit: builder_config.gas_limit(parent_header.gas_limit),
191                parent_beacon_block_root: attributes.parent_beacon_block_root(),
192                withdrawals: attributes.withdrawals.clone().map(Into::into),
193                extra_data: builder_config.extra_data,
194            },
195        )
196        .map_err(PayloadBuilderError::other)?;
197
198    let chain_spec = client.chain_spec();
199
200    debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
201    let mut cumulative_gas_used = 0;
202    let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
203    let base_fee = builder.evm_mut().block().basefee();
204
205    let mut best_txs = best_txs(BestTransactionsAttributes::new(
206        base_fee,
207        builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
208    ));
209    let mut total_fees = U256::ZERO;
210
211    // If we have a sparse trie handle, wire a state hook that streams per-tx state diffs
212    // to the background trie pipeline for incremental state root computation.
213    if let Some(ref handle) = trie_handle {
214        builder.executor_mut().set_state_hook(Some(Box::new(handle.state_hook())));
215    }
216
217    builder.apply_pre_execution_changes().map_err(|err| {
218        warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
219        PayloadBuilderError::Internal(err.into())
220    })?;
221
222    // initialize empty blob sidecars at first. If cancun is active then this will be populated by
223    // blob sidecars if any.
224    let mut blob_sidecars = BlobSidecars::Empty;
225
226    let mut block_blob_count = 0;
227    let mut block_transactions_rlp_length = 0;
228
229    let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
230    let protocol_max_blob_count =
231        blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_else(Default::default);
232
233    // Apply user-configured blob limit (EIP-7872)
234    // Per EIP-7872: if the minimum is zero, set it to one
235    let max_blob_count = builder_config
236        .max_blobs_per_block
237        .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
238        .unwrap_or(protocol_max_blob_count);
239
240    let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
241
242    let withdrawals_rlp_length =
243        attributes.withdrawals.as_ref().map(|withdrawals| withdrawals.length()).unwrap_or(0);
244
245    while let Some(pool_tx) = best_txs.next() {
246        // ensure we still have capacity for this transaction
247        if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
248            // we can't fit this transaction into the block, so we need to mark it as invalid
249            // which also removes all dependent transaction from the iterator before we can
250            // continue
251            best_txs.mark_invalid(
252                &pool_tx,
253                &InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
254            );
255            continue
256        }
257
258        // check if the job was cancelled, if so we can exit early
259        if cancel.is_cancelled() {
260            return Ok(BuildOutcome::Cancelled)
261        }
262
263        // convert tx to a signed transaction
264        let tx = pool_tx.to_consensus();
265
266        let tx_rlp_len = tx.inner().length();
267
268        let estimated_block_size_with_tx =
269            block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; // 1Kb of overhead for the block header
270
271        if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
272            best_txs.mark_invalid(
273                &pool_tx,
274                &InvalidPoolTransactionError::OversizedData {
275                    size: estimated_block_size_with_tx,
276                    limit: MAX_RLP_BLOCK_SIZE,
277                },
278            );
279            continue
280        }
281
282        // There's only limited amount of blob space available per block, so we need to check if
283        // the EIP-4844 can still fit in the block
284        let mut blob_tx_sidecar = None;
285        let tx_blob_count = tx.blob_count();
286
287        if let Some(tx_blob_count) = tx_blob_count {
288            if block_blob_count + tx_blob_count > max_blob_count {
289                // we can't fit this _blob_ transaction into the block, so we mark it as
290                // invalid, which removes its dependent transactions from
291                // the iterator. This is similar to the gas limit condition
292                // for regular transactions above.
293                trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
294                best_txs.mark_invalid(
295                    &pool_tx,
296                    &InvalidPoolTransactionError::Eip4844(
297                        Eip4844PoolTransactionError::TooManyEip4844Blobs {
298                            have: block_blob_count + tx_blob_count,
299                            permitted: max_blob_count,
300                        },
301                    ),
302                );
303                continue
304            }
305
306            let blob_sidecar_result = 'sidecar: {
307                let Some(sidecar) =
308                    pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
309                else {
310                    break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
311                };
312
313                if is_osaka {
314                    if sidecar.is_eip7594() {
315                        Ok(sidecar)
316                    } else {
317                        Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
318                    }
319                } else if sidecar.is_eip4844() {
320                    Ok(sidecar)
321                } else {
322                    Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
323                }
324            };
325
326            blob_tx_sidecar = match blob_sidecar_result {
327                Ok(sidecar) => Some(sidecar),
328                Err(error) => {
329                    best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
330                    continue
331                }
332            };
333        }
334
335        let miner_fee = tx.effective_tip_per_gas(base_fee);
336        let tx_hash = *tx.tx_hash();
337
338        let gas_used = match builder.execute_transaction(tx) {
339            Ok(gas_used) => gas_used,
340            Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
341                error, ..
342            })) => {
343                if error.is_nonce_too_low() {
344                    // if the nonce is too low, we can skip this transaction
345                    trace!(target: "payload_builder", %error, ?tx_hash, "skipping nonce too low transaction");
346                } else {
347                    // if the transaction is invalid, we can skip it and all of its
348                    // descendants
349                    trace!(target: "payload_builder", %error, ?tx_hash, "skipping invalid transaction and its descendants");
350                    best_txs.mark_invalid(
351                        &pool_tx,
352                        &InvalidPoolTransactionError::Consensus(
353                            InvalidTransactionError::TxTypeNotSupported,
354                        ),
355                    );
356                }
357                continue
358            }
359            // this is an error that we should treat as fatal for this attempt
360            Err(err) => return Err(PayloadBuilderError::evm(err)),
361        };
362
363        // add to the total blob gas used if the transaction successfully executed
364        if let Some(blob_count) = tx_blob_count {
365            block_blob_count += blob_count;
366
367            // if we've reached the max blob count, we can skip blob txs entirely
368            if block_blob_count == max_blob_count {
369                best_txs.skip_blobs();
370            }
371        }
372
373        block_transactions_rlp_length += tx_rlp_len;
374
375        // update and add to total fees
376        let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
377        total_fees += U256::from(miner_fee) * U256::from(gas_used);
378        cumulative_gas_used += gas_used;
379
380        // Add blob tx sidecar to the payload.
381        if let Some(sidecar) = blob_tx_sidecar {
382            blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
383        }
384    }
385
386    // check if we have a better block
387    if !is_better_payload(best_payload.as_ref(), total_fees) {
388        // Release db
389        drop(builder);
390        // can skip building the block
391        return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
392    }
393
394    let BlockBuilderOutcome { execution_result, block, .. } = if let Some(mut handle) = trie_handle
395    {
396        // Drop the state hook, which drops the StateHookSender and triggers
397        // FinishedStateUpdates via its Drop impl, signaling the trie task to finalize.
398        builder.executor_mut().set_state_hook(None);
399
400        // The sparse trie has been computing incrementally alongside tx execution.
401        // This recv() waits for the final root hash — most work is already done.
402        // Fall back to sync state root if the trie pipeline fails.
403        match handle.state_root() {
404            Ok(outcome) => {
405                debug!(target: "payload_builder", id=%payload_id, state_root=?outcome.state_root, "received state root from sparse trie");
406                builder.finish(
407                    state_provider.as_ref(),
408                    Some((outcome.state_root, Arc::unwrap_or_clone(outcome.trie_updates))),
409                )?
410            }
411            Err(err) => {
412                warn!(target: "payload_builder", id=%payload_id, %err, "sparse trie failed, falling back to sync state root");
413                builder.finish(state_provider.as_ref(), None)?
414            }
415        }
416    } else {
417        builder.finish(state_provider.as_ref(), None)?
418    };
419
420    let requests = chain_spec
421        .is_prague_active_at_timestamp(attributes.timestamp)
422        .then_some(execution_result.requests);
423
424    let sealed_block = Arc::new(block.into_sealed_block());
425    debug!(target: "payload_builder", id=%payload_id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
426
427    if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE {
428        return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
429            rlp_length: sealed_block.rlp_length(),
430            max_rlp_length: MAX_RLP_BLOCK_SIZE,
431        }));
432    }
433
434    let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
435        // add blob sidecars from the executed txs
436        .with_sidecars(blob_sidecars);
437
438    Ok(BuildOutcome::Better { payload, cached_reads })
439}