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, doc_auto_cfg))]
10#![allow(clippy::useless_let_if_seq)]
11
12pub mod validator;
13pub use validator::EthereumExecutionPayloadValidator;
14
15use alloy_consensus::{Transaction, Typed2718};
16use alloy_primitives::U256;
17use reth_basic_payload_builder::{
18    is_better_payload, BuildArguments, BuildOutcome, PayloadBuilder, PayloadConfig,
19};
20use reth_chainspec::{ChainSpec, ChainSpecProvider, EthChainSpec, EthereumHardforks};
21use reth_errors::{BlockExecutionError, BlockValidationError};
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::{EthBuiltPayload, EthPayloadBuilderAttributes};
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_primitives_traits::SignedTransaction;
32use reth_revm::{database::StateProviderDatabase, db::State};
33use reth_storage_api::StateProviderFactory;
34use reth_transaction_pool::{
35    error::InvalidPoolTransactionError, BestTransactions, BestTransactionsAttributes,
36    PoolTransaction, TransactionPool, ValidPoolTransaction,
37};
38use revm::context_interface::Block as _;
39use std::sync::Arc;
40use tracing::{debug, trace, warn};
41
42mod config;
43pub use config::*;
44use reth_primitives_traits::transaction::error::InvalidTransactionError;
45use reth_transaction_pool::error::Eip4844PoolTransactionError;
46
47type BestTransactionsIter<Pool> = Box<
48    dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
49>;
50
51/// Ethereum payload builder
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
54    /// Client providing access to node state.
55    client: Client,
56    /// Transaction pool.
57    pool: Pool,
58    /// The type responsible for creating the evm.
59    evm_config: EvmConfig,
60    /// Payload builder configuration.
61    builder_config: EthereumBuilderConfig,
62}
63
64impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
65    /// `EthereumPayloadBuilder` constructor.
66    pub const fn new(
67        client: Client,
68        pool: Pool,
69        evm_config: EvmConfig,
70        builder_config: EthereumBuilderConfig,
71    ) -> Self {
72        Self { client, pool, evm_config, builder_config }
73    }
74}
75
76// Default implementation of [PayloadBuilder] for unit type
77impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
78where
79    EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
80    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec> + Clone,
81    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
82{
83    type Attributes = EthPayloadBuilderAttributes;
84    type BuiltPayload = EthBuiltPayload;
85
86    fn try_build(
87        &self,
88        args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
89    ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
90        default_ethereum_payload(
91            self.evm_config.clone(),
92            self.client.clone(),
93            self.pool.clone(),
94            self.builder_config.clone(),
95            args,
96            |attributes| self.pool.best_transactions_with_attributes(attributes),
97        )
98    }
99
100    fn build_empty_payload(
101        &self,
102        config: PayloadConfig<Self::Attributes>,
103    ) -> Result<EthBuiltPayload, PayloadBuilderError> {
104        let args = BuildArguments::new(Default::default(), config, Default::default(), None);
105
106        default_ethereum_payload(
107            self.evm_config.clone(),
108            self.client.clone(),
109            self.pool.clone(),
110            self.builder_config.clone(),
111            args,
112            |attributes| self.pool.best_transactions_with_attributes(attributes),
113        )?
114        .into_payload()
115        .ok_or_else(|| PayloadBuilderError::MissingPayload)
116    }
117}
118
119/// Constructs an Ethereum transaction payload using the best transactions from the pool.
120///
121/// Given build arguments including an Ethereum client, transaction pool,
122/// and configuration, this function creates a transaction payload. Returns
123/// a result indicating success with the payload or an error in case of failure.
124#[inline]
125pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
126    evm_config: EvmConfig,
127    client: Client,
128    pool: Pool,
129    builder_config: EthereumBuilderConfig,
130    args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
131    best_txs: F,
132) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
133where
134    EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
135    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
136    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
137    F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
138{
139    let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
140    let PayloadConfig { parent_header, attributes } = config;
141
142    let state_provider = client.state_by_block_hash(parent_header.hash())?;
143    let state = StateProviderDatabase::new(&state_provider);
144    let mut db =
145        State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
146
147    let mut builder = evm_config
148        .builder_for_next_block(
149            &mut db,
150            &parent_header,
151            NextBlockEnvAttributes {
152                timestamp: attributes.timestamp(),
153                suggested_fee_recipient: attributes.suggested_fee_recipient(),
154                prev_randao: attributes.prev_randao(),
155                gas_limit: builder_config.gas_limit(parent_header.gas_limit),
156                parent_beacon_block_root: attributes.parent_beacon_block_root(),
157                withdrawals: Some(attributes.withdrawals().clone()),
158            },
159        )
160        .map_err(PayloadBuilderError::other)?;
161
162    let chain_spec = client.chain_spec();
163
164    debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
165    let mut cumulative_gas_used = 0;
166    let block_gas_limit: u64 = builder.evm_mut().block().gas_limit;
167    let base_fee = builder.evm_mut().block().basefee;
168
169    let mut best_txs = best_txs(BestTransactionsAttributes::new(
170        base_fee,
171        builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
172    ));
173    let mut total_fees = U256::ZERO;
174
175    builder.apply_pre_execution_changes().map_err(|err| {
176        warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
177        PayloadBuilderError::Internal(err.into())
178    })?;
179
180    let mut block_blob_count = 0;
181    let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
182    let max_blob_count =
183        blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_default();
184
185    while let Some(pool_tx) = best_txs.next() {
186        // ensure we still have capacity for this transaction
187        if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
188            // we can't fit this transaction into the block, so we need to mark it as invalid
189            // which also removes all dependent transaction from the iterator before we can
190            // continue
191            best_txs.mark_invalid(
192                &pool_tx,
193                InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
194            );
195            continue
196        }
197
198        // check if the job was cancelled, if so we can exit early
199        if cancel.is_cancelled() {
200            return Ok(BuildOutcome::Cancelled)
201        }
202
203        // convert tx to a signed transaction
204        let tx = pool_tx.to_consensus();
205
206        // There's only limited amount of blob space available per block, so we need to check if
207        // the EIP-4844 can still fit in the block
208        if let Some(blob_tx) = tx.as_eip4844() {
209            let tx_blob_count = blob_tx.blob_versioned_hashes.len() as u64;
210
211            if block_blob_count + tx_blob_count > max_blob_count {
212                // we can't fit this _blob_ transaction into the block, so we mark it as
213                // invalid, which removes its dependent transactions from
214                // the iterator. This is similar to the gas limit condition
215                // for regular transactions above.
216                trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
217                best_txs.mark_invalid(
218                    &pool_tx,
219                    InvalidPoolTransactionError::Eip4844(
220                        Eip4844PoolTransactionError::TooManyEip4844Blobs {
221                            have: block_blob_count + tx_blob_count,
222                            permitted: max_blob_count,
223                        },
224                    ),
225                );
226                continue
227            }
228        }
229
230        let gas_used = match builder.execute_transaction(tx.clone()) {
231            Ok(gas_used) => gas_used,
232            Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
233                error, ..
234            })) => {
235                if error.is_nonce_too_low() {
236                    // if the nonce is too low, we can skip this transaction
237                    trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
238                } else {
239                    // if the transaction is invalid, we can skip it and all of its
240                    // descendants
241                    trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
242                    best_txs.mark_invalid(
243                        &pool_tx,
244                        InvalidPoolTransactionError::Consensus(
245                            InvalidTransactionError::TxTypeNotSupported,
246                        ),
247                    );
248                }
249                continue
250            }
251            // this is an error that we should treat as fatal for this attempt
252            Err(err) => return Err(PayloadBuilderError::evm(err)),
253        };
254
255        // add to the total blob gas used if the transaction successfully executed
256        if let Some(blob_tx) = tx.as_eip4844() {
257            block_blob_count += blob_tx.blob_versioned_hashes.len() as u64;
258
259            // if we've reached the max blob count, we can skip blob txs entirely
260            if block_blob_count == max_blob_count {
261                best_txs.skip_blobs();
262            }
263        }
264
265        // update add to total fees
266        let miner_fee =
267            tx.effective_tip_per_gas(base_fee).expect("fee is always valid; execution succeeded");
268        total_fees += U256::from(miner_fee) * U256::from(gas_used);
269        cumulative_gas_used += gas_used;
270    }
271
272    // check if we have a better block
273    if !is_better_payload(best_payload.as_ref(), total_fees) {
274        // Release db
275        drop(builder);
276        // can skip building the block
277        return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
278    }
279
280    let BlockBuilderOutcome { execution_result, block, .. } = builder.finish(&state_provider)?;
281
282    let requests = chain_spec
283        .is_prague_active_at_timestamp(attributes.timestamp)
284        .then_some(execution_result.requests);
285
286    // initialize empty blob sidecars at first. If cancun is active then this will
287    let mut blob_sidecars = Vec::new();
288
289    // only determine cancun fields when active
290    if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp) {
291        // grab the blob sidecars from the executed txs
292        blob_sidecars = pool
293            .get_all_blobs_exact(
294                block
295                    .body()
296                    .transactions()
297                    .filter(|tx| tx.is_eip4844())
298                    .map(|tx| *tx.tx_hash())
299                    .collect(),
300            )
301            .map_err(PayloadBuilderError::other)?;
302    }
303
304    let sealed_block = Arc::new(block.sealed_block().clone());
305    debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
306
307    let mut payload = EthBuiltPayload::new(attributes.id, sealed_block, total_fees, requests);
308
309    // extend the payload with the blob sidecars from the executed txs
310    payload.extend_sidecars(blob_sidecars.into_iter().map(Arc::unwrap_or_clone));
311
312    Ok(BuildOutcome::Better { payload, cached_reads })
313}