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