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